From 0097c7d1b7fc7d6939918cfaf630ebc1efae618b Mon Sep 17 00:00:00 2001
From: Steven Rothwell <45801433+steven-rothwell@users.noreply.github.com>
Date: Sat, 21 Oct 2023 10:45:27 -0400
Subject: [PATCH] Version 1.0.0
---
.gitignore | 398 +++
.vscode/launch.json | 36 +
.vscode/tasks.json | 41 +
CODEOWNERS | 3 +
Crud.Api/Attributes/PreventCrudAttribute.cs | 53 +
Crud.Api/Attributes/PreventQueryAttribute.cs | 31 +
Crud.Api/Constants/Delimiter.cs | 8 +
Crud.Api/Constants/ErrorMessage.cs | 17 +
Crud.Api/Constants/JsonSerializerOption.cs | 12 +
Crud.Api/Constants/Namespace.cs | 7 +
Crud.Api/Constants/RegexOption.cs | 7 +
Crud.Api/Controllers/BaseApiController.cs | 39 +
Crud.Api/Controllers/CrudController.cs | 556 ++++
Crud.Api/Crud.Api.csproj | 14 +
Crud.Api/Enums/CrudOperation.cs | 29 +
Crud.Api/Extensions/BsonDocumentExtensions.cs | 13 +
Crud.Api/Extensions/GenericExtensions.cs | 28 +
Crud.Api/Extensions/ObjectExtensions.cs | 54 +
Crud.Api/Extensions/PropertyInfoExtensions.cs | 83 +
Crud.Api/Extensions/StringExtensions.cs | 93 +
Crud.Api/Extensions/TypeExtensions.cs | 52 +
Crud.Api/Helpers/ReflectionHelper.cs | 16 +
Crud.Api/Models/Address.cs | 13 +
Crud.Api/Models/ExternalEntity.cs | 11 +
Crud.Api/Models/IExternalEntity.cs | 7 +
Crud.Api/Models/User.cs | 21 +
Crud.Api/Options/ApplicationOptions.cs | 11 +
Crud.Api/Options/MongoDbOptions.cs | 8 +
Crud.Api/Preservers/IPreserver.cs | 20 +
Crud.Api/Preservers/MongoDb/Preserver.cs | 218 ++
Crud.Api/Program.cs | 68 +
Crud.Api/Properties/launchSettings.json | 31 +
Crud.Api/QueryModels/Condition.cs | 34 +
Crud.Api/QueryModels/GroupedCondition.cs | 17 +
Crud.Api/QueryModels/Operator.cs | 75 +
Crud.Api/QueryModels/Query.cs | 35 +
Crud.Api/QueryModels/Sort.cs | 17 +
Crud.Api/Results/MessageResult.cs | 21 +
Crud.Api/Results/ValidationResult.cs | 21 +
Crud.Api/Services/IMongoDbService.cs | 23 +
Crud.Api/Services/IPostprocessingService.cs | 21 +
Crud.Api/Services/IPreprocessingService.cs | 21 +
Crud.Api/Services/IQueryCollectionService.cs | 7 +
Crud.Api/Services/IStreamService.cs | 9 +
Crud.Api/Services/ITypeService.cs | 7 +
Crud.Api/Services/MongoDbService.cs | 320 ++
Crud.Api/Services/PostprocessingService.cs | 69 +
Crud.Api/Services/PreprocessingService.cs | 69 +
Crud.Api/Services/QueryCollectionService.cs | 12 +
Crud.Api/Services/StreamService.cs | 17 +
Crud.Api/Services/TypeService.cs | 26 +
Crud.Api/Validators/IValidator.cs | 23 +
Crud.Api/Validators/Validator.cs | 356 ++
Crud.Api/appsettings.Development.json | 19 +
Crud.Api/appsettings.json | 20 +
.../Attributes/PreventCrudAttributeTests.cs | 115 +
.../Attributes/PreventQueryAttributeTests.cs | 41 +
.../Controllers/BaseApiControllerTests.cs | 57 +
.../Controllers/CrudControllerTests.cs | 2026 ++++++++++++
.../Crud.Api.Tests/Crud.Api.Tests.csproj | 31 +
.../Extensions/BsonDocumentExtensionsTests.cs | 29 +
.../Extensions/GenericExtensionsTests.cs | 80 +
.../Extensions/ObjectExtensionsTests.cs | 229 ++
.../Extensions/PropertyInfoExtensionsTests.cs | 313 ++
.../Extensions/StringExtensionsTests.cs | 202 ++
.../Extensions/TypeExtensionsTests.cs | 134 +
.../Helpers/RelectionHelperTests.cs | 54 +
.../Services/MongoDbServiceTests.cs | 1437 +++++++++
.../Services/TypeServiceTests.cs | 54 +
.../Crud.Api.Tests/TestingModels/Model.cs | 13 +
Crud.Tests/Crud.Api.Tests/Usings.cs | 1 +
.../Validators/ValidatorTests.cs | 961 ++++++
Crud.sln | 33 +
LICENSE | 21 +
MetricsInstructions.txt | 3 +
MetricsResults.txt | 14 +
Postman/Crud.postman_collection.json | 2852 +++++++++++++++++
Postman/Crud_Local.postman_environment.json | 74 +
README.md | 530 +++
docs/CONTRIBUTING.md | 34 +
docs/POSTPROCESSING.md | 17 +
docs/PREPROCESSING.md | 17 +
docs/PREVENTCRUDATTRIBUTE.md | 17 +
docs/PREVENTQUERYATTRIBUTE.md | 23 +
docs/release-notes/RELEASE-1.0.0.md | 25 +
85 files changed, 12674 insertions(+)
create mode 100644 .gitignore
create mode 100644 .vscode/launch.json
create mode 100644 .vscode/tasks.json
create mode 100644 CODEOWNERS
create mode 100644 Crud.Api/Attributes/PreventCrudAttribute.cs
create mode 100644 Crud.Api/Attributes/PreventQueryAttribute.cs
create mode 100644 Crud.Api/Constants/Delimiter.cs
create mode 100644 Crud.Api/Constants/ErrorMessage.cs
create mode 100644 Crud.Api/Constants/JsonSerializerOption.cs
create mode 100644 Crud.Api/Constants/Namespace.cs
create mode 100644 Crud.Api/Constants/RegexOption.cs
create mode 100644 Crud.Api/Controllers/BaseApiController.cs
create mode 100644 Crud.Api/Controllers/CrudController.cs
create mode 100644 Crud.Api/Crud.Api.csproj
create mode 100644 Crud.Api/Enums/CrudOperation.cs
create mode 100644 Crud.Api/Extensions/BsonDocumentExtensions.cs
create mode 100644 Crud.Api/Extensions/GenericExtensions.cs
create mode 100644 Crud.Api/Extensions/ObjectExtensions.cs
create mode 100644 Crud.Api/Extensions/PropertyInfoExtensions.cs
create mode 100644 Crud.Api/Extensions/StringExtensions.cs
create mode 100644 Crud.Api/Extensions/TypeExtensions.cs
create mode 100644 Crud.Api/Helpers/ReflectionHelper.cs
create mode 100644 Crud.Api/Models/Address.cs
create mode 100644 Crud.Api/Models/ExternalEntity.cs
create mode 100644 Crud.Api/Models/IExternalEntity.cs
create mode 100644 Crud.Api/Models/User.cs
create mode 100644 Crud.Api/Options/ApplicationOptions.cs
create mode 100644 Crud.Api/Options/MongoDbOptions.cs
create mode 100644 Crud.Api/Preservers/IPreserver.cs
create mode 100644 Crud.Api/Preservers/MongoDb/Preserver.cs
create mode 100644 Crud.Api/Program.cs
create mode 100644 Crud.Api/Properties/launchSettings.json
create mode 100644 Crud.Api/QueryModels/Condition.cs
create mode 100644 Crud.Api/QueryModels/GroupedCondition.cs
create mode 100644 Crud.Api/QueryModels/Operator.cs
create mode 100644 Crud.Api/QueryModels/Query.cs
create mode 100644 Crud.Api/QueryModels/Sort.cs
create mode 100644 Crud.Api/Results/MessageResult.cs
create mode 100644 Crud.Api/Results/ValidationResult.cs
create mode 100644 Crud.Api/Services/IMongoDbService.cs
create mode 100644 Crud.Api/Services/IPostprocessingService.cs
create mode 100644 Crud.Api/Services/IPreprocessingService.cs
create mode 100644 Crud.Api/Services/IQueryCollectionService.cs
create mode 100644 Crud.Api/Services/IStreamService.cs
create mode 100644 Crud.Api/Services/ITypeService.cs
create mode 100644 Crud.Api/Services/MongoDbService.cs
create mode 100644 Crud.Api/Services/PostprocessingService.cs
create mode 100644 Crud.Api/Services/PreprocessingService.cs
create mode 100644 Crud.Api/Services/QueryCollectionService.cs
create mode 100644 Crud.Api/Services/StreamService.cs
create mode 100644 Crud.Api/Services/TypeService.cs
create mode 100644 Crud.Api/Validators/IValidator.cs
create mode 100644 Crud.Api/Validators/Validator.cs
create mode 100644 Crud.Api/appsettings.Development.json
create mode 100644 Crud.Api/appsettings.json
create mode 100644 Crud.Tests/Crud.Api.Tests/Attributes/PreventCrudAttributeTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Attributes/PreventQueryAttributeTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Controllers/BaseApiControllerTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Controllers/CrudControllerTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Crud.Api.Tests.csproj
create mode 100644 Crud.Tests/Crud.Api.Tests/Extensions/BsonDocumentExtensionsTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Extensions/GenericExtensionsTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Extensions/ObjectExtensionsTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Extensions/PropertyInfoExtensionsTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Extensions/StringExtensionsTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Extensions/TypeExtensionsTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Helpers/RelectionHelperTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Services/MongoDbServiceTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Services/TypeServiceTests.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/TestingModels/Model.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Usings.cs
create mode 100644 Crud.Tests/Crud.Api.Tests/Validators/ValidatorTests.cs
create mode 100644 Crud.sln
create mode 100644 LICENSE
create mode 100644 MetricsInstructions.txt
create mode 100644 MetricsResults.txt
create mode 100644 Postman/Crud.postman_collection.json
create mode 100644 Postman/Crud_Local.postman_environment.json
create mode 100644 README.md
create mode 100644 docs/CONTRIBUTING.md
create mode 100644 docs/POSTPROCESSING.md
create mode 100644 docs/PREPROCESSING.md
create mode 100644 docs/PREVENTCRUDATTRIBUTE.md
create mode 100644 docs/PREVENTQUERYATTRIBUTE.md
create mode 100644 docs/release-notes/RELEASE-1.0.0.md
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8dd4607
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,398 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..c106c8f
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,36 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ // Use IntelliSense to find out which attributes exist for C# debugging
+ // Use hover for the description of the existing attributes
+ // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
+ "name": ".NET Core Launch (web)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ // If you have changed target frameworks, make sure to update the program path.
+ "program": "${workspaceFolder}/Crud.Api/bin/Debug/net7.0/Crud.Api.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/Crud.Api",
+ "stopAtEntry": false,
+ // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
+ "serverReadyAction": {
+ "action": "openExternally",
+ "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
+ },
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "sourceFileMap": {
+ "/Views": "${workspaceFolder}/Views"
+ },
+ "launchSettingsProfile": "Crud.Api"
+ },
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach"
+ }
+ ]
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..593e547
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,41 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/Crud.Api/Crud.Api.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "publish",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "publish",
+ "${workspaceFolder}/Crud.Api/Crud.Api.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "watch",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "watch",
+ "run",
+ "--project",
+ "${workspaceFolder}/Crud.Api/Crud.Api.csproj"
+ ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..30832d0
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,3 @@
+# Global ownership
+
+* @steven-rothwell
\ No newline at end of file
diff --git a/Crud.Api/Attributes/PreventCrudAttribute.cs b/Crud.Api/Attributes/PreventCrudAttribute.cs
new file mode 100644
index 0000000..98a38eb
--- /dev/null
+++ b/Crud.Api/Attributes/PreventCrudAttribute.cs
@@ -0,0 +1,53 @@
+using Crud.Api.Enums;
+
+namespace Crud.Api.Attributes
+{
+ ///
+ /// Decorate a class to prevent CRUD operations.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+ public class PreventCrudAttribute : Attribute
+ {
+ private HashSet _preventedCrudOperations;
+
+ private static IReadOnlyDictionary _encompassingCrudOperationLookup = new Dictionary
+ {
+ { CrudOperation.ReadWithId, CrudOperation.Read },
+ { CrudOperation.ReadWithQueryParams, CrudOperation.Read },
+ { CrudOperation.ReadWithQuery, CrudOperation.Read },
+ { CrudOperation.ReadCount, CrudOperation.Read },
+ { CrudOperation.PartialUpdateWithId, CrudOperation.PartialUpdate },
+ { CrudOperation.PartialUpdateWithQueryParams, CrudOperation.PartialUpdate },
+ { CrudOperation.DeleteWithId, CrudOperation.Delete },
+ { CrudOperation.DeleteWithQueryParams, CrudOperation.Delete },
+ { CrudOperation.DeleteWithQuery, CrudOperation.Delete }
+ };
+
+ ///
+ /// Specific CRUD operations to prevent may be specified. If no operations are specified, all CRUD operations are prevented.
+ ///
+ /// CRUD operations to be prevented.
+ public PreventCrudAttribute(params CrudOperation[] crudOperations)
+ {
+ _preventedCrudOperations = new HashSet(crudOperations);
+ }
+
+ public Boolean AllowsCrudOperation(CrudOperation crudOperation)
+ {
+ if (_preventedCrudOperations.Count == 0)
+ return false;
+
+ if (_preventedCrudOperations.Contains(crudOperation))
+ return false;
+
+ if (_encompassingCrudOperationLookup.ContainsKey(crudOperation))
+ {
+ var encompassingCrudOperation = _encompassingCrudOperationLookup[crudOperation];
+
+ return !_preventedCrudOperations.Contains(encompassingCrudOperation);
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/Crud.Api/Attributes/PreventQueryAttribute.cs b/Crud.Api/Attributes/PreventQueryAttribute.cs
new file mode 100644
index 0000000..7bd50d2
--- /dev/null
+++ b/Crud.Api/Attributes/PreventQueryAttribute.cs
@@ -0,0 +1,31 @@
+namespace Crud.Api.Attributes
+{
+ ///
+ /// Decorate a property to prevent Query operators.
+ ///
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+ public class PreventQueryAttribute : Attribute
+ {
+ private HashSet _preventedOperators;
+
+ ///
+ /// Specific Query operators to prevent may be specified. If no operators are specified, all Query operators are prevented. Suggest using constants.
+ ///
+ /// Query operators to be prevented.
+ public PreventQueryAttribute(params String[] operators)
+ {
+ _preventedOperators = new HashSet(operators);
+ }
+
+ public Boolean AllowsOperator(String @operator)
+ {
+ if (_preventedOperators.Count == 0)
+ return false;
+
+ if (_preventedOperators.Contains(@operator))
+ return false;
+
+ return true;
+ }
+ }
+}
diff --git a/Crud.Api/Constants/Delimiter.cs b/Crud.Api/Constants/Delimiter.cs
new file mode 100644
index 0000000..05bf34a
--- /dev/null
+++ b/Crud.Api/Constants/Delimiter.cs
@@ -0,0 +1,8 @@
+namespace Crud.Api.Constants
+{
+ public static class Delimiter
+ {
+ public const Char QueryParamChildProperty = '_';
+ public const Char MongoDbChildProperty = '.';
+ }
+}
diff --git a/Crud.Api/Constants/ErrorMessage.cs b/Crud.Api/Constants/ErrorMessage.cs
new file mode 100644
index 0000000..a1e9145
--- /dev/null
+++ b/Crud.Api/Constants/ErrorMessage.cs
@@ -0,0 +1,17 @@
+using Crud.Api.QueryModels;
+
+namespace Crud.Api.Constants
+{
+ public static class ErrorMessage
+ {
+ public const String NotFoundRead = "No matching {0} found.";
+ public const String NotFoundUpdate = "No matching {0} found to update.";
+ public const String NotFoundDelete = "No matching {0} found to delete.";
+
+ public const String BadRequestModelType = "No model type found.";
+ public const String BadRequestBody = "Request body cannot be null or whitespace.";
+ public const String BadRequestQuery = $"A {nameof(Query)} object could not be created from the request body. Reason: {{0}}";
+
+ public const String MethodNotAllowedType = "{0} is not allowed on type {1}.";
+ }
+}
diff --git a/Crud.Api/Constants/JsonSerializerOption.cs b/Crud.Api/Constants/JsonSerializerOption.cs
new file mode 100644
index 0000000..8735226
--- /dev/null
+++ b/Crud.Api/Constants/JsonSerializerOption.cs
@@ -0,0 +1,12 @@
+using System.Text.Json;
+
+namespace Crud.Api.Constants
+{
+ public static class JsonSerializerOption
+ {
+ public static readonly JsonSerializerOptions Default = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ };
+ }
+}
diff --git a/Crud.Api/Constants/Namespace.cs b/Crud.Api/Constants/Namespace.cs
new file mode 100644
index 0000000..f2dd392
--- /dev/null
+++ b/Crud.Api/Constants/Namespace.cs
@@ -0,0 +1,7 @@
+namespace Crud.Api.Constants
+{
+ public static class Namespace
+ {
+ public const String Models = "Crud.Api.Models";
+ }
+}
diff --git a/Crud.Api/Constants/RegexOption.cs b/Crud.Api/Constants/RegexOption.cs
new file mode 100644
index 0000000..8762c9b
--- /dev/null
+++ b/Crud.Api/Constants/RegexOption.cs
@@ -0,0 +1,7 @@
+namespace Crud.Api.Constants
+{
+ public static class RegexOption
+ {
+ public const String CaseInsensitive = "i";
+ }
+}
diff --git a/Crud.Api/Controllers/BaseApiController.cs b/Crud.Api/Controllers/BaseApiController.cs
new file mode 100644
index 0000000..5eeb707
--- /dev/null
+++ b/Crud.Api/Controllers/BaseApiController.cs
@@ -0,0 +1,39 @@
+using Crud.Api.Options;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+
+namespace Crud.Api.Controllers
+{
+ public abstract class BaseApiController : ControllerBase
+ {
+ protected readonly ApplicationOptions _applicationOptions;
+
+ public BaseApiController(IOptions applicationOptions)
+ {
+ _applicationOptions = applicationOptions.Value;
+ }
+
+ protected virtual IActionResult InternalServerError()
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError);
+ }
+
+ protected virtual IActionResult InternalServerError(Exception exception)
+ {
+ if (_applicationOptions.ShowExceptions)
+ return StatusCode(StatusCodes.Status500InternalServerError, exception.ToString());
+
+ return InternalServerError();
+ }
+
+ protected virtual IActionResult InternalServerError(String? message)
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError, message);
+ }
+
+ protected virtual IActionResult MethodNotAllowed(String? message)
+ {
+ return StatusCode(StatusCodes.Status405MethodNotAllowed, message);
+ }
+ }
+}
diff --git a/Crud.Api/Controllers/CrudController.cs b/Crud.Api/Controllers/CrudController.cs
new file mode 100644
index 0000000..397fc00
--- /dev/null
+++ b/Crud.Api/Controllers/CrudController.cs
@@ -0,0 +1,556 @@
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Crud.Api.Constants;
+using Crud.Api.Enums;
+using Crud.Api.Helpers;
+using Crud.Api.Options;
+using Crud.Api.Preservers;
+using Crud.Api.QueryModels;
+using Crud.Api.Results;
+using Crud.Api.Services;
+using Crud.Api.Validators;
+using Humanizer;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+
+namespace Crud.Api.Controllers;
+
+[ApiController]
+[Route("api")]
+public class CrudController : BaseApiController
+{
+ private readonly ILogger _logger;
+ private readonly IValidator _validator;
+ private readonly IPreserver _preserver;
+ private readonly IStreamService _streamService;
+ private readonly ITypeService _typeService;
+ private readonly IQueryCollectionService _queryCollectionService;
+ private readonly IPreprocessingService _preprocessingService;
+ private readonly IPostprocessingService _postprocessingService;
+
+ public CrudController(IOptions applicationOptions, ILogger logger, IValidator validator, IPreserver preserver, IStreamService streamService, ITypeService typeService, IQueryCollectionService queryCollectionService,
+ IPreprocessingService preprocessingService, IPostprocessingService postprocessingService)
+ : base(applicationOptions)
+ {
+ _logger = logger;
+ _validator = validator;
+ _preserver = preserver;
+ _streamService = streamService;
+ _typeService = typeService;
+ _queryCollectionService = queryCollectionService;
+ _preprocessingService = preprocessingService;
+ _postprocessingService = postprocessingService;
+ }
+
+ [Route("{typeName}"), HttpPost]
+ public async Task CreateAsync(String typeName)
+ {
+ try
+ {
+ var type = _typeService.GetModelType(typeName);
+ if (type is null)
+ return BadRequest(ErrorMessage.BadRequestModelType);
+
+ var crudOperation = CrudOperation.Create;
+ if (!type.AllowsCrudOperation(crudOperation))
+ return MethodNotAllowed(String.Format(ErrorMessage.MethodNotAllowedType, crudOperation.ToString().Humanize(), type.Name));
+
+ string json = await _streamService.ReadToEndThenDisposeAsync(Request.Body, Encoding.UTF8);
+ if (String.IsNullOrWhiteSpace(json))
+ return BadRequest(ErrorMessage.BadRequestBody);
+
+ dynamic? model = JsonSerializer.Deserialize(json, type, JsonSerializerOption.Default);
+
+ var validationResult = (ValidationResult)await _validator.ValidateCreateAsync(model);
+ if (!validationResult.IsValid)
+ return BadRequest(validationResult.Message);
+
+ var preprocessingMessageResult = (MessageResult)await _preprocessingService.PreprocessCreateAsync(model);
+ if (!preprocessingMessageResult.IsSuccessful)
+ return InternalServerError(preprocessingMessageResult.Message);
+
+ var createdModel = await _preserver.CreateAsync(model);
+
+ var postprocessingMessageResult = (MessageResult)await _postprocessingService.PostprocessCreateAsync(createdModel);
+ if (!postprocessingMessageResult.IsSuccessful)
+ return InternalServerError(postprocessingMessageResult.Message);
+
+ return Ok(createdModel);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"Error creating with typeName: {typeName}.");
+ return InternalServerError(ex);
+ }
+ }
+
+ [Route("{typeName}/{id:guid}"), HttpGet]
+ public async Task ReadAsync(String typeName, Guid id)
+ {
+ try
+ {
+ var type = _typeService.GetModelType(typeName);
+ if (type is null)
+ return BadRequest(ErrorMessage.BadRequestModelType);
+
+ var crudOperation = CrudOperation.ReadWithId;
+ if (!type.AllowsCrudOperation(crudOperation))
+ return MethodNotAllowed(String.Format(ErrorMessage.MethodNotAllowedType, crudOperation.ToString().Humanize(), type.Name));
+
+ dynamic model = Convert.ChangeType(Activator.CreateInstance(type, null), type)!;
+
+ var preprocessingMessageResult = (MessageResult)await _preprocessingService.PreprocessReadAsync(model, id);
+ if (!preprocessingMessageResult.IsSuccessful)
+ return InternalServerError(preprocessingMessageResult.Message);
+
+ var readAsync = ReflectionHelper.GetGenericMethod(type, typeof(IPreserver), nameof(IPreserver.ReadAsync), new Type[] { typeof(Guid) });
+ model = await (dynamic)readAsync.Invoke(_preserver, new object[] { id });
+
+ if (model is null)
+ return NotFound(String.Format(ErrorMessage.NotFoundRead, typeName));
+
+ var postprocessingMessageResult = (MessageResult)await _postprocessingService.PostprocessReadAsync(model, id);
+ if (!postprocessingMessageResult.IsSuccessful)
+ return InternalServerError(postprocessingMessageResult.Message);
+
+ return Ok(model);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"Error reading with typeName: {typeName}, id: {id}.");
+ return InternalServerError(ex);
+ }
+ }
+
+ [Route("{typeName}"), HttpGet]
+ public async Task ReadAsync(String typeName)
+ {
+ try
+ {
+ var type = _typeService.GetModelType(typeName);
+ if (type is null)
+ return BadRequest(ErrorMessage.BadRequestModelType);
+
+ var crudOperation = CrudOperation.ReadWithQueryParams;
+ if (!type.AllowsCrudOperation(crudOperation))
+ return MethodNotAllowed(String.Format(ErrorMessage.MethodNotAllowedType, crudOperation.ToString().Humanize(), type.Name));
+
+ var queryParams = _queryCollectionService.ConvertToDictionary(Request.Query);
+
+ dynamic model = Convert.ChangeType(Activator.CreateInstance(type, null), type)!;
+
+ var validationResult = (ValidationResult)await _validator.ValidateReadAsync(model!, queryParams);
+ if (!validationResult.IsValid)
+ return BadRequest(validationResult.Message);
+
+ var preprocessingMessageResult = (MessageResult)await _preprocessingService.PreprocessReadAsync(model!, queryParams);
+ if (!preprocessingMessageResult.IsSuccessful)
+ return InternalServerError(preprocessingMessageResult.Message);
+
+ var readAsync = ReflectionHelper.GetGenericMethod(type, typeof(IPreserver), nameof(IPreserver.ReadAsync), new Type[] { typeof(IDictionary) });
+ var models = await (dynamic)readAsync.Invoke(_preserver, new object[] { queryParams });
+
+ var postprocessingMessageResult = (MessageResult)await _postprocessingService.PostprocessReadAsync(models, queryParams);
+ if (!postprocessingMessageResult.IsSuccessful)
+ return InternalServerError(postprocessingMessageResult.Message);
+
+ return Ok(models);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"Error reading with typeName: {typeName}.");
+ return InternalServerError(ex);
+ }
+ }
+
+ [Route("query/{typeName}"), HttpPost]
+ public async Task QueryReadAsync(String typeName)
+ {
+ try
+ {
+ var type = _typeService.GetModelType(typeName);
+ if (type is null)
+ return BadRequest(ErrorMessage.BadRequestModelType);
+
+ var crudOperation = CrudOperation.ReadWithQuery;
+ if (!type.AllowsCrudOperation(crudOperation))
+ return MethodNotAllowed(String.Format(ErrorMessage.MethodNotAllowedType, crudOperation.ToString().Humanize(), type.Name));
+
+ string json = await _streamService.ReadToEndThenDisposeAsync(Request.Body, Encoding.UTF8);
+ if (String.IsNullOrWhiteSpace(json))
+ return BadRequest(ErrorMessage.BadRequestBody);
+
+ Query? query = null;
+ string jsonExMessage = $"{nameof(Query)} is null.";
+ try { query = JsonSerializer.Deserialize(json, typeof(Query), JsonSerializerOption.Default) as Query; }
+ catch (Exception jsonEx)
+ {
+ jsonExMessage = jsonEx.Message;
+ }
+ if (query is null)
+ return BadRequest(String.Format(ErrorMessage.BadRequestQuery, jsonExMessage));
+
+ dynamic model = Convert.ChangeType(Activator.CreateInstance(type, null), type)!;
+
+ if (_applicationOptions.ValidateQuery)
+ {
+ var validationResult = (ValidationResult)_validator.ValidateQuery(model!, query);
+ if (!validationResult.IsValid)
+ return BadRequest(validationResult.Message);
+ }
+
+ var preprocessingMessageResult = (MessageResult)await _preprocessingService.PreprocessReadAsync(model!, query);
+ if (!preprocessingMessageResult.IsSuccessful)
+ return InternalServerError(preprocessingMessageResult.Message);
+
+ var queryReadAsync = ReflectionHelper.GetGenericMethod(type, typeof(IPreserver), nameof(IPreserver.QueryReadAsync), new Type[] { typeof(Query) });
+ var models = await (dynamic)queryReadAsync.Invoke(_preserver, new object[] { query });
+
+ var postprocessingMessageResult = (MessageResult)await _postprocessingService.PostprocessReadAsync(models, query);
+ if (!postprocessingMessageResult.IsSuccessful)
+ return InternalServerError(postprocessingMessageResult.Message);
+
+ if ((query.Includes is not null && query.Includes.Count > 0) || (query.Excludes is not null && query.Excludes.Count > 0))
+ {
+ var modelsWithLessProperties = JsonSerializer.Deserialize