aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/LICENSE.txt195
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/README.md5
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IBulkDataStore.cs65
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs123
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs56
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IUserEntity.cs39
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs48
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs507
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Extensions.cs81
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/IDbModel.cs52
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/ProtectedDbStore.cs70
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs86
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/SQL/DbExtensions.cs521
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/SQL/EnumerableTable.cs118
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlColumnNameAttribute.cs54
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlTableNameAttribute.cs40
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlVariable.cs58
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/SQL/TableManager.cs66
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/Blob.cs244
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobExtensions.cs67
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobStore.cs162
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/LWDecriptorCreationException.cs45
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs48
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs230
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageEntry.cs44
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs347
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageRemoveFailedException.cs44
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageUpdateFailedException.cs43
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/Storage/UndefinedBlobStateException.cs45
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs99
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj49
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.xml476
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading.Sql/LICENSE.txt195
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading.Sql/README.md5
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs174
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj41
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/LICENSE.txt195
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/README.md5
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs162
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs199
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs47
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Events/ConfigurableAsyncIntervalAttribute.cs51
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs124
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Events/IIntervalScheduleable.cs45
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Events/IntervalResultionType.cs49
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs334
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs102
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs161
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs35
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs61
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs93
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj39
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.xml260
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs473
-rw-r--r--lib/VNLib.Plugins.Extensions.VNCache/LICENSE.txt195
-rw-r--r--lib/VNLib.Plugins.Extensions.VNCache/README.md5
-rw-r--r--lib/VNLib.Plugins.Extensions.VNCache/src/VNCacheExtensions.cs108
-rw-r--r--lib/VNLib.Plugins.Extensions.VNCache/src/VNLib.Plugins.Extensions.VNCache.csproj26
-rw-r--r--lib/VNLib.Plugins.Extensions.VNCache/src/VnCacheClient.cs231
-rw-r--r--lib/VNLib.Plugins.Extensions.Validation/LICENSE.txt195
-rw-r--r--lib/VNLib.Plugins.Extensions.Validation/README.md9
-rw-r--r--lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj36
-rw-r--r--lib/VNLib.Plugins.Extensions.Validation/src/ValErrWebMessage.cs41
-rw-r--r--lib/VNLib.Plugins.Extensions.Validation/src/ValidationErrorMessage.cs36
-rw-r--r--lib/VNLib.Plugins.Extensions.Validation/src/ValidationExtensions.cs78
-rw-r--r--lib/VNLib.Plugins.Extensions.Validation/src/ValidatorExtensions.cs205
-rw-r--r--lib/VNLib.Plugins.Extentions.TransactionalEmail/LICENSE.txt195
-rw-r--r--lib/VNLib.Plugins.Extentions.TransactionalEmail/README.md5
-rw-r--r--lib/VNLib.Plugins.Extentions.TransactionalEmail/src/EmailSystemConfig.cs54
-rw-r--r--lib/VNLib.Plugins.Extentions.TransactionalEmail/src/TransactionalEmailExtensions.cs161
-rw-r--r--lib/VNLib.Plugins.Extentions.TransactionalEmail/src/VNLib.Plugins.Extentions.TransactionalEmail.csproj36
71 files changed, 8593 insertions, 0 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Data/LICENSE.txt b/lib/VNLib.Plugins.Extensions.Data/LICENSE.txt
new file mode 100644
index 0000000..147bcd6
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/LICENSE.txt
@@ -0,0 +1,195 @@
+Copyright (c) 2022 Vaughn Nugent
+
+Contact information
+ Name: Vaughn Nugent
+ Email: public[at]vaughnnugent[dot]com
+ Website: https://www.vaughnnugent.com
+
+The software in this repository is licensed under the GNU Affero GPL version 3.0 (or any later version).
+
+GNU AFFERO GENERAL PUBLIC LICENSE
+
+Version 3, 19 November 2007
+
+Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+Preamble
+
+The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
+
+The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
+
+An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
+
+The precise terms and conditions for copying, distribution and modification follow.
+TERMS AND CONDITIONS
+0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the Program.
+
+To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
+1. Source Code.
+
+The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+2. Basic Permissions.
+
+All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
+4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
+5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
+ b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
+ c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
+ d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
+6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
+ b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
+ c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
+ d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
+ e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
+7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
+ b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
+ c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
+ d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
+ e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
+ f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
+12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
+13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
+
+Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/README.md b/lib/VNLib.Plugins.Extensions.Data/README.md
new file mode 100644
index 0000000..f69b8dd
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/README.md
@@ -0,0 +1,5 @@
+# VNLib.Plugins.Extensions.Data
+*An extension library for adding common data structures and abstractions for building remote data stores with sql/data extensions*
+
+#### Builds
+Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my [website](https://www.vaughnnugent.com/resources/software). All tar-gzip (.tgz) files will have an associated .sha384 appended checksum of the desired download file. \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IBulkDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IBulkDataStore.cs
new file mode 100644
index 0000000..b644ec3
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IBulkDataStore.cs
@@ -0,0 +1,65 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: IBulkDataStore.cs
+*
+* IBulkDataStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+using VNLib.Utils;
+
+namespace VNLib.Plugins.Extensions.Data.Abstractions
+{
+ /// <summary>
+ /// An abstraction that defines a Data-Store that supports
+ /// bulk data operations
+ /// </summary>
+ /// <typeparam name="T">The data-model type</typeparam>
+ public interface IBulkDataStore<T>
+ {
+ /// <summary>
+ /// Deletes a collection of records from the store
+ /// </summary>
+ /// <param name="records">A collection of records to delete</param>
+ ///<returns>A task the resolves the number of entires removed from the store</returns>
+ Task<ERRNO> DeleteBulkAsync(ICollection<T> records);
+ /// <summary>
+ /// Updates a collection of records
+ /// </summary>
+ /// <param name="records">The collection of records to update</param>
+ /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ Task<ERRNO> UpdateBulkAsync(ICollection<T> records);
+ /// <summary>
+ /// Creates a bulk collection of records as entries in the store
+ /// </summary>
+ /// <param name="records">The collection of records to add</param>
+ /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ Task<ERRNO> CreateBulkAsync(ICollection<T> records);
+ /// <summary>
+ /// Creates or updates individual records from a bulk collection of records
+ /// </summary>
+ /// <param name="records">The collection of records to add</param>
+ /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ Task<ERRNO> AddOrUpdateBulkAsync(ICollection<T> records);
+ }
+
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs
new file mode 100644
index 0000000..4ab62bf
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs
@@ -0,0 +1,123 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: IDataStore.cs
+*
+* IDataStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using VNLib.Utils;
+
+namespace VNLib.Plugins.Extensions.Data.Abstractions
+{
+ /// <summary>
+ /// An abstraction that defines a Data-Store and common
+ /// operations that retrieve or manipulate records of data
+ /// </summary>
+ /// <typeparam name="T">The data-model type</typeparam>
+ public interface IDataStore<T>
+ {
+ /// <summary>
+ /// Gets the total number of records in the current store
+ /// </summary>
+ /// <returns>A task that resolves the number of records in the store</returns>
+ Task<long> GetCountAsync();
+ /// <summary>
+ /// Gets the number of records that belong to the specified constraint
+ /// </summary>
+ /// <param name="specifier">A specifier to constrain the reults</param>
+ /// <returns>The number of records that belong to the specifier</returns>
+ Task<long> GetCountAsync(string specifier);
+ /// <summary>
+ /// Gets a record from its key
+ /// </summary>
+ /// <param name="key">The key identifying the unique record</param>
+ /// <returns>A promise that resolves the record identified by the specified key</returns>
+ Task<T?> GetSingleAsync(string key);
+ /// <summary>
+ /// Gets a record from its key
+ /// </summary>
+ /// <param name="specifiers">A variable length specifier arguemnt array for retreiving a single application</param>
+ /// <returns></returns>
+ Task<T?> GetSingleAsync(params string[] specifiers);
+ /// <summary>
+ /// Gets a record from the store with a partial model, intended to complete the model
+ /// </summary>
+ /// <param name="record">The partial model used to query the store</param>
+ /// <returns>A task the resolves the completed data-model</returns>
+ Task<T?> GetSingleAsync(T record);
+ /// <summary>
+ /// Fills a collection with enires retireved from the store using the specifer
+ /// </summary>
+ /// <param name="collection">The collection to add entires to</param>
+ /// <param name="specifier">A specifier argument to constrain results</param>
+ /// <param name="limit">The maximum number of elements to retrieve</param>
+ /// <returns>A Task the resolves to the number of items added to the collection</returns>
+ Task<ERRNO> GetCollectionAsync(ICollection<T> collection, string specifier, int limit);
+ /// <summary>
+ /// Fills a collection with enires retireved from the store using a variable length specifier
+ /// parameter
+ /// </summary>
+ /// <param name="collection">The collection to add entires to</param>
+ /// <param name="limit">The maximum number of elements to retrieve</param>
+ /// <param name="args"></param>
+ /// <returns>A Task the resolves to the number of items added to the collection</returns>
+ Task<ERRNO> GetCollectionAsync(ICollection<T> collection, int limit, params string[] args);
+ /// <summary>
+ /// Updates an entry in the store with the specified record
+ /// </summary>
+ /// <param name="record">The record to update</param>
+ /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ Task<ERRNO> UpdateAsync(T record);
+ /// <summary>
+ /// Creates a new entry in the store representing the specified record
+ /// </summary>
+ /// <param name="record">The record to add to the store</param>
+ /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ Task<ERRNO> CreateAsync(T record);
+ /// <summary>
+ /// Deletes one or more entrires from the store matching the specified record
+ /// </summary>
+ /// <param name="record">The record to remove from the store</param>
+ /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
+ Task<ERRNO> DeleteAsync(T record);
+ /// <summary>
+ /// Deletes one or more entires from the store matching the specified unique key
+ /// </summary>
+ /// <param name="key">The unique key that identifies the record</param>
+ /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
+ Task<ERRNO> DeleteAsync(string key);
+ /// <summary>
+ /// Deletes one or more entires from the store matching the supplied specifiers
+ /// </summary>
+ /// <param name="specifiers">A variable length array of specifiers used to delete one or more entires</param>
+ /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
+ Task<ERRNO> DeleteAsync(params string[] specifiers);
+ /// <summary>
+ /// Updates an entry in the store if it exists, or creates a new entry if one does not already exist
+ /// </summary>
+ /// <param name="record">The record to add to the store</param>
+ /// <returns>A task the resolves the result of the operation</returns>
+ Task<ERRNO> AddOrUpdateAsync(T record);
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs
new file mode 100644
index 0000000..4a9cb89
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs
@@ -0,0 +1,56 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: IPaginatedDataStore.cs
+*
+* IPaginatedDataStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace VNLib.Plugins.Extensions.Data.Abstractions
+{
+ /// <summary>
+ /// Defines a Data-Store that can retirieve and manipulate paginated
+ /// data
+ /// </summary>
+ /// <typeparam name="T">The data-model type</typeparam>
+ public interface IPaginatedDataStore<T>
+ {
+ /// <summary>
+ /// Gets a collection of records using a pagination style query, and adds the records to the collecion
+ /// </summary>
+ /// <param name="collection">The collection to add records to</param>
+ /// <param name="page">Pagination page to get records from</param>
+ /// <param name="limit">The maximum number of items to retrieve from the store</param>
+ /// <returns>A task that resolves the number of items added to the collection</returns>
+ Task<int> GetPageAsync(ICollection<T> collection, int page, int limit);
+ /// <summary>
+ /// Gets a collection of records using a pagination style query with constraint arguments, and adds the records to the collecion
+ /// </summary>
+ /// <param name="collection">The collection to add records to</param>
+ /// <param name="page">Pagination page to get records from</param>
+ /// <param name="limit">The maximum number of items to retrieve from the store</param>
+ /// <param name="constraints">A params array of strings to constrain the result set from the db</param>
+ /// <returns>A task that resolves the number of items added to the collection</returns>
+ Task<int> GetPageAsync(ICollection<T> collection, int page, int limit, params string[] constraints);
+ }
+
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IUserEntity.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IUserEntity.cs
new file mode 100644
index 0000000..aaca7c0
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IUserEntity.cs
@@ -0,0 +1,39 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: IUserEntity.cs
+*
+* IUserEntity.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Data.Abstractions
+{
+ /// <summary>
+ /// Defines an entity base that has an owner, identified by its user-id
+ /// </summary>
+ public interface IUserEntity
+ {
+ /// <summary>
+ /// The user-id of the owner of the entity
+ /// </summary>
+ string? UserId { get; set; }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs
new file mode 100644
index 0000000..b48231b
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs
@@ -0,0 +1,48 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: DbModelBase.cs
+*
+* DbModelBase.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+
+namespace VNLib.Plugins.Extensions.Data
+{
+ /// <summary>
+ /// Provides a base for DBSet Records with a timestamp/version
+ /// a unique ID key, and create/modified timestamps
+ /// </summary>
+ public abstract class DbModelBase : IDbModel
+ {
+ ///<inheritdoc/>
+ public abstract string Id { get; set; }
+ ///<inheritdoc/>
+ [Timestamp]
+ [JsonIgnore]
+ public virtual byte[]? Version { get; set; }
+ ///<inheritdoc/>
+ public abstract DateTime Created { get; set; }
+ ///<inheritdoc/>
+ public abstract DateTime LastModified { get; set; }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs
new file mode 100644
index 0000000..8cf4e2e
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs
@@ -0,0 +1,507 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: DbStore.cs
+*
+* DbStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory.Caching;
+using VNLib.Plugins.Extensions.Data.Abstractions;
+
+namespace VNLib.Plugins.Extensions.Data
+{
+ /// <summary>
+ /// Implements basic data-store functionality with abstract query builders
+ /// </summary>
+ /// <typeparam name="T">A <see cref="DbModelBase"/> implemented type</typeparam>
+ public abstract class DbStore<T> : IDataStore<T>, IPaginatedDataStore<T> where T: class, IDbModel
+ {
+ /// <summary>
+ /// Gets a unique ID for a new record being added to the store
+ /// </summary>
+ public abstract string RecordIdBuilder { get; }
+ /// <summary>
+ /// Gets a new <see cref="TransactionalDbContext"/> ready for use
+ /// </summary>
+ /// <returns></returns>
+ public abstract TransactionalDbContext NewContext();
+
+ /// <summary>
+ /// An object rental for entity collections
+ /// </summary>
+ public ObjectRental<List<T>> ListRental { get; } = ObjectRental.Create<List<T>>(null, static ret => ret.Clear());
+
+ #region Add Or Update
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> AddOrUpdateAsync(T record)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ IQueryable<T> query;
+ if (string.IsNullOrWhiteSpace(record.Id))
+ {
+ //Get the application
+ query = AddOrUpdateQueryBuilder(ctx, record);
+ }
+ else
+ {
+ //Get the application
+ query = (from et in ctx.Set<T>()
+ where et.Id == record.Id
+ select et);
+ }
+ //Using single
+ T? entry = await query.SingleOrDefaultAsync();
+ //Check if creted
+ if (entry == null)
+ {
+ //Create a new template id
+ record.Id = RecordIdBuilder;
+ //Set the created/lm times
+ record.Created = record.LastModified = DateTime.UtcNow;
+ //Add the new template to the ctx
+ ctx.Add(record);
+ }
+ else
+ {
+ OnRecordUpdate(record, entry);
+ }
+ //Save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //commit transaction if update was successful
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> UpdateAsync(T record)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the application
+ IQueryable<T> query = UpdateQueryBuilder(ctx, record);
+ //Using single to make sure only one app is in the db (should never be an issue)
+ T? oldEntry = await query.SingleOrDefaultAsync();
+ if (oldEntry == null)
+ {
+ return false;
+ }
+ //Update the template meta-data
+ OnRecordUpdate(record, oldEntry);
+ //Only publish update if changes happened
+ if (!ctx.ChangeTracker.HasChanges())
+ {
+ //commit transaction if no changes need to be made
+ await ctx.CommitTransactionAsync();
+ return true;
+ }
+ //Save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //commit transaction if update was successful
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> CreateAsync(T record)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Create a new template id
+ record.Id = RecordIdBuilder;
+ //Update the created/last modified time of the record
+ record.Created = record.LastModified = DateTime.UtcNow;
+ //Add the new template
+ ctx.Add(record);
+ //save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //Commit transaction
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Builds a query that attempts to get a single entry from the
+ /// store based on the specified record if it does not have a
+ /// valid <see cref="DbModelBase.Id"/> property
+ /// </summary>
+ /// <param name="context">The active context to query</param>
+ /// <param name="record">The record to search for</param>
+ /// <returns>A query that yields a single record if it exists in the store</returns>
+ protected virtual IQueryable<T> AddOrUpdateQueryBuilder(TransactionalDbContext context, T record)
+ {
+ //default to get single of the specific record
+ return GetSingleQueryBuilder(context, record);
+ }
+ /// <summary>
+ /// Builds a query that attempts to get a single entry from the
+ /// store to update based on the specified record
+ /// </summary>
+ /// <param name="context">The active context to query</param>
+ /// <param name="record">The record to search for</param>
+ /// <returns>A query that yields a single record to update if it exists in the store</returns>
+ protected virtual IQueryable<T> UpdateQueryBuilder(TransactionalDbContext context, T record)
+ {
+ //default to get single of the specific record
+ return GetSingleQueryBuilder(context, record);
+ }
+ /// <summary>
+ /// Updates the current record (if found) to the new record before
+ /// storing the updates.
+ /// </summary>
+ /// <param name="newRecord">The new record to capture data from</param>
+ /// <param name="currentRecord">The current record to be updated</param>
+ protected abstract void OnRecordUpdate(T newRecord, T currentRecord);
+ #endregion
+
+ #region Delete
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> DeleteAsync(string key)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the template by its id
+ IQueryable<T> query = (from temp in ctx.Set<T>()
+ where temp.Id == key
+ select temp);
+ T? record = await query.SingleOrDefaultAsync();
+ if (record == null)
+ {
+ return false;
+ }
+ //Add the new application
+ ctx.Remove(record);
+ //Save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //Commit transaction
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> DeleteAsync(T record)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get a query for a a single item
+ IQueryable<T> query = GetSingleQueryBuilder(ctx, record);
+ //Get the entry
+ T? entry = await query.SingleOrDefaultAsync();
+ if (entry == null)
+ {
+ return false;
+ }
+ //Add the new application
+ ctx.Remove(entry);
+ //Save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //Commit transaction
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> DeleteAsync(params string[] specifiers)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the template by its id
+ IQueryable<T> query = DeleteQueryBuilder(ctx, specifiers);
+ T? entry = await query.SingleOrDefaultAsync();
+ if (entry == null)
+ {
+ return false;
+ }
+ //Add the new application
+ ctx.Remove(entry);
+ //Save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //Commit transaction
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Builds a query that results in a single entry to delete from the
+ /// constraint arguments
+ /// </summary>
+ /// <param name="context">The active context</param>
+ /// <param name="constraints">A variable length parameter array of query constraints</param>
+ /// <returns>A query that yields a single record (or no record) to delete</returns>
+ protected virtual IQueryable<T> DeleteQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ {
+ //default use the get-single method, as the implementation is usually identical
+ return GetSingleQueryBuilder(context, constraints);
+ }
+ #endregion
+
+ #region Get Collection
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> GetCollectionAsync(ICollection<T> collection, string specifier, int limit)
+ {
+ int previous = collection.Count;
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ await GetCollectionQueryBuilder(ctx, specifier).Take(limit).Select(static e => e).ForEachAsync(collection.Add);
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ //Return the number of elements add to the collection
+ return collection.Count - previous;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> GetCollectionAsync(ICollection<T> collection, int limit, params string[] args)
+ {
+ int previous = collection.Count;
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ await GetCollectionQueryBuilder(ctx, args).Take(limit).Select(static e => e).ForEachAsync(collection.Add);
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ //Return the number of elements add to the collection
+ return collection.Count - previous;
+ }
+
+ /// <summary>
+ /// Builds a query to get a count of records constrained by the specifier
+ /// </summary>
+ /// <param name="context">The active context to run the query on</param>
+ /// <param name="specifier">The specifier constrain</param>
+ /// <returns>A query that can be counted</returns>
+ protected virtual IQueryable<T> GetCollectionQueryBuilder(TransactionalDbContext context, string specifier)
+ {
+ return GetCollectionQueryBuilder(context, new string[] { specifier });
+ }
+
+ /// <summary>
+ /// Builds a query to get a collection of records based on an variable length array of parameters
+ /// </summary>
+ /// <param name="context">The active context to run the query on</param>
+ /// <param name="constraints">An arguments array to constrain the results of the query</param>
+ /// <returns>A query that returns a collection of records from the store</returns>
+ protected abstract IQueryable<T> GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints);
+
+ #endregion
+
+ #region Get Count
+ ///<inheritdoc/>
+ public virtual async Task<long> GetCountAsync()
+ {
+ //Open db connection
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Async get the number of records of the given entity type
+ long count = await ctx.Set<T>().LongCountAsync();
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ return count;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<long> GetCountAsync(string specifier)
+ {
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Async get the number of records of the given entity type
+ long count = await GetCountQueryBuilder(ctx, specifier).LongCountAsync();
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ return count;
+ }
+
+ /// <summary>
+ /// Builds a query to get a count of records constrained by the specifier
+ /// </summary>
+ /// <param name="context">The active context to run the query on</param>
+ /// <param name="specifier">The specifier constrain</param>
+ /// <returns>A query that can be counted</returns>
+ protected virtual IQueryable<T> GetCountQueryBuilder(TransactionalDbContext context, string specifier)
+ {
+ //Default use the get collection and just call the count method
+ return GetCollectionQueryBuilder(context, specifier);
+ }
+ #endregion
+
+ #region Get Single
+ ///<inheritdoc/>
+ public virtual async Task<T?> GetSingleAsync(string key)
+ {
+ //Open db connection
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ T? record = await (from entry in ctx.Set<T>()
+ where entry.Id == key
+ select entry)
+ .SingleOrDefaultAsync();
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ return record;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<T?> GetSingleAsync(T record)
+ {
+ //Open db connection
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ T? entry = await GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync();
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ return record;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<T?> GetSingleAsync(params string[] specifiers)
+ {
+ //Open db connection
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ T? record = await GetSingleQueryBuilder(ctx, specifiers).SingleOrDefaultAsync();
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ return record;
+ }
+ /// <summary>
+ /// Builds a query to get a single record from the variable length parameter arguments
+ /// </summary>
+ /// <param name="context">The context to execute query against</param>
+ /// <param name="constraints">Arguments to constrain the results of the query to a single record</param>
+ /// <returns>A query that yields a single record</returns>
+ protected abstract IQueryable<T> GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints);
+ /// <summary>
+ /// <para>
+ /// Builds a query to get a single record from the specified record.
+ /// </para>
+ /// <para>
+ /// Unless overridden, performs an ID based query for a single entry
+ /// </para>
+ /// </summary>
+ /// <param name="context">The context to execute query against</param>
+ /// <param name="record">A record to referrence the lookup</param>
+ /// <returns>A query that yields a single record</returns>
+ protected virtual IQueryable<T> GetSingleQueryBuilder(TransactionalDbContext context, T record)
+ {
+ return from entry in context.Set<T>()
+ where entry.Id == record.Id
+ select entry;
+ }
+ #endregion
+
+ #region Get Page
+ ///<inheritdoc/>
+ public virtual async Task<int> GetPageAsync(ICollection<T> collection, int page, int limit)
+ {
+ //Store preivous count
+ int previous = collection.Count;
+ //Open db connection
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get a page offset and a limit for the
+ await ctx.Set<T>()
+ .Skip(page * limit)
+ .Take(limit)
+ .Select(static p => p)
+ .ForEachAsync(collection.Add);
+
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ //Return the number of records added
+ return collection.Count - previous;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<int> GetPageAsync(ICollection<T> collection, int page, int limit, params string[] constraints)
+ {
+ //Store preivous count
+ int previous = collection.Count;
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ await GetPageQueryBuilder(ctx, constraints)
+ .Skip(page * limit)
+ .Take(limit)
+ .Select(static e => e)
+ .ForEachAsync(collection.Add);
+
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ //Return the number of records added
+ return collection.Count - previous;
+ }
+ /// <summary>
+ /// Builds a query to get a collection of records based on an variable length array of parameters
+ /// </summary>
+ /// <param name="context">The active context to run the query on</param>
+ /// <param name="constraints">An arguments array to constrain the results of the query</param>
+ /// <returns>A query that returns a paginated collection of records from the store</returns>
+ protected virtual IQueryable<T> GetPageQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ {
+ //Default to getting the entire collection and just selecting a single page
+ return GetCollectionQueryBuilder(context, constraints);
+ }
+ #endregion
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Extensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions.cs
new file mode 100644
index 0000000..1e0c8de
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Extensions.cs
@@ -0,0 +1,81 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: Extensions.cs
+*
+* Extensions.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Utils;
+using VNLib.Plugins.Extensions.Data.Abstractions;
+
+namespace VNLib.Plugins.Extensions.Data
+{
+ public static class Extensions
+ {
+
+ public static int GetPageOrDefault(this IReadOnlyDictionary<string, string> queryArgs, int @default, int minClamp = 0, int maxClamp = int.MaxValue)
+ {
+ return queryArgs.TryGetValue("page", out string? pageStr) && int.TryParse(pageStr, out int page) ? Math.Clamp(page, minClamp, maxClamp) : @default;
+ }
+
+ public static int GetLimitOrDefault(this IReadOnlyDictionary<string, string> queryArgs, int @default, int minClamp = 0, int maxClamp = int.MaxValue)
+ {
+ return queryArgs.TryGetValue("limit", out string? limitStr) && int.TryParse(limitStr, out int limit) ? Math.Clamp(limit, minClamp, maxClamp) : @default;
+ }
+
+ public static async Task<ERRNO> AddBulkAsync<TEntity>(this DbStore<TEntity> store, IEnumerable<TEntity> records, string userId, bool overwriteTime = true)
+ where TEntity : class, IDbModel, IUserEntity
+ {
+ //Open context and transaction
+ await using TransactionalDbContext database = store.NewContext();
+ await database.OpenTransactionAsync();
+ //Get the entity set
+ DbSet<TEntity> set = database.Set<TEntity>();
+ //Generate random ids for the feeds and set user-id
+ foreach (TEntity entity in records)
+ {
+ entity.Id = store.RecordIdBuilder;
+ //Explicitly assign the user-id
+ entity.UserId = userId;
+ //If the entity has the default created time, update it, otherwise leave it as is
+ if (overwriteTime || entity.Created == default)
+ {
+ entity.Created = DateTime.UtcNow;
+ }
+ //Update last-modified time
+ entity.LastModified = DateTime.UtcNow;
+ }
+ //Add feeds to database
+ set.AddRange(records);
+ //Commit changes
+ ERRNO count = await database.SaveChangesAsync();
+ //Commit transaction and exit
+ await database.CommitTransactionAsync();
+ return count;
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/IDbModel.cs b/lib/VNLib.Plugins.Extensions.Data/src/IDbModel.cs
new file mode 100644
index 0000000..8dbc2e4
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/IDbModel.cs
@@ -0,0 +1,52 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: IDbModel.cs
+*
+* IDbModel.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Data
+{
+ /// <summary>
+ /// Represents a basic data model for an EFCore entity
+ /// for support in data-stores
+ /// </summary>
+ public interface IDbModel
+ {
+ /// <summary>
+ /// A unique id for the entity
+ /// </summary>
+ string Id { get; set; }
+ /// <summary>
+ /// The <see cref="DateTime"/> the entity was created in the store
+ /// </summary>
+ DateTime Created { get; set; }
+ /// <summary>
+ /// The <see cref="DateTime"/> the entity was last modified in the store
+ /// </summary>
+ DateTime LastModified { get; set; }
+ /// <summary>
+ /// Entity concurrency token
+ /// </summary>
+ byte[]? Version { get; set; }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/ProtectedDbStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/ProtectedDbStore.cs
new file mode 100644
index 0000000..654d52c
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/ProtectedDbStore.cs
@@ -0,0 +1,70 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: ProtectedDbStore.cs
+*
+* ProtectedDbStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+
+using VNLib.Plugins.Extensions.Data.Abstractions;
+
+namespace VNLib.Plugins.Extensions.Data
+{
+#nullable enable
+ /// <summary>
+ /// A data store that provides unique identities and protections based on an entity that has an owner <see cref="IUserEntity"/>
+ /// </summary>
+ public abstract class ProtectedDbStore<TEntity> : DbStore<TEntity> where TEntity : class, IDbModel, IUserEntity
+ {
+ ///<inheritdoc/>
+ protected override IQueryable<TEntity> GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ {
+ string userId = constraints[0];
+ //Query items for the user and its id
+ return from item in context.Set<TEntity>()
+ where item.UserId == userId
+ orderby item.Created descending
+ select item;
+ }
+
+ /// <summary>
+ /// Gets a single item contrained by a given user-id and item id
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="constraints"></param>
+ /// <returns></returns>
+ protected override IQueryable<TEntity> GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ {
+ string key = constraints[0];
+ string userId = constraints[1];
+ //Query items for the user and its id
+ return from item in context.Set<TEntity>()
+ where item.Id == key && item.UserId == userId
+ select item;
+ }
+ ///<inheritdoc/>
+ protected override IQueryable<TEntity> GetSingleQueryBuilder(TransactionalDbContext context, TEntity record)
+ {
+ return this.GetSingleQueryBuilder(context, record.Id, record.UserId);
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs
new file mode 100644
index 0000000..7c4f4a3
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs
@@ -0,0 +1,86 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: ProtectedEntityExtensions.cs
+*
+* ProtectedEntityExtensions.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils;
+using VNLib.Plugins.Extensions.Data.Abstractions;
+
+namespace VNLib.Plugins.Extensions.Data
+{
+ public static class ProtectedEntityExtensions
+ {
+ /// <summary>
+ /// Updates the specified record within the store
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="record">The record to update</param>
+ /// <param name="userId">The userid of the record owner</param>
+ /// <returns>A task that evaluates to the number of records modified</returns>
+ public static Task<ERRNO> UpdateAsync<TEntity>(this IDataStore<TEntity> store, TEntity record, string userId) where TEntity : class, IDbModel, IUserEntity
+ {
+ record.UserId = userId;
+ return store.UpdateAsync(record);
+ }
+
+ /// <summary>
+ /// Updates the specified record within the store
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="record">The record to update</param>
+ /// <param name="userId">The userid of the record owner</param>
+ /// <returns>A task that evaluates to the number of records modified</returns>
+ public static Task<ERRNO> CreateAsync<TEntity>(this IDataStore<TEntity> store, TEntity record, string userId) where TEntity : class, IDbModel, IUserEntity
+ {
+ record.UserId = userId;
+ return store.CreateAsync(record);
+ }
+
+ /// <summary>
+ /// Gets a single entity from its ID and user-id
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="key">The unique id of the entity</param>
+ /// <param name="userId">The user's id that owns the resource</param>
+ /// <returns>A task that resolves the entity or null if not found</returns>
+ public static Task<TEntity?> GetSingleAsync<TEntity>(this IDataStore<TEntity> store, string key, string userId) where TEntity : class, IDbModel, IUserEntity
+ {
+ return store.GetSingleAsync(key, userId);
+ }
+
+ /// <summary>
+ /// Deletes a single entiry by its ID only if it belongs to the speicifed user
+ /// </summary>
+ /// <param name="store"></param>
+ /// <param name="key">The unique id of the entity</param>
+ /// <param name="userId">The user's id that owns the resource</param>
+ /// <returns>A task the resolves the number of eneities deleted (should evaluate to true or false)</returns>
+ public static Task<ERRNO> DeleteAsync<TEntity>(this IDataStore<TEntity> store, string key, string userId) where TEntity : class, IDbModel, IUserEntity
+ {
+ return store.DeleteAsync(key, userId);
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/DbExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/DbExtensions.cs
new file mode 100644
index 0000000..1f9164e
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/SQL/DbExtensions.cs
@@ -0,0 +1,521 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: DbExtensions.cs
+*
+* DbExtensions.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Data;
+using System.Reflection;
+using System.Data.Common;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory.Caching;
+
+namespace VNLib.Plugins.Extensions.Data.SQL
+{
+ /// <summary>
+ /// Provides basic extension methods for ADO.NET abstract classes
+ /// for rapid development
+ /// </summary>
+ public static class DbExtensions
+ {
+ /*
+ * Object rental for propery dictionaries used for custom result objects
+ */
+ private static ObjectRental<Dictionary<string, PropertyInfo>> DictStore { get; } = ObjectRental.Create<Dictionary<string, PropertyInfo>>(null, static dict => dict.Clear(), 20);
+
+
+ /// <summary>
+ /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Input"/> with the specified value
+ /// and adds it to the command.
+ /// </summary>
+ /// <param name="cmd"></param>
+ /// <param name="name">The parameter name</param>
+ /// <param name="value">The value of the parameter</param>
+ /// <param name="type">The <see cref="DbType"/> of the column</param>
+ /// <param name="nullable">Are null types allowed in the value parameter</param>
+ /// <returns>The created parameter</returns>
+ public static DbParameter AddParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, bool @nullable = false)
+ {
+ //Create the new parameter from command
+ DbParameter param = cmd.CreateParameter();
+ //Set parameter variables
+ param.ParameterName = name;
+ param.Value = value;
+ param.DbType = type;
+ //Force non null mapping
+ param.SourceColumnNullMapping = nullable;
+ //Specify input parameter
+ param.Direction = ParameterDirection.Input;
+ //Add param to list
+ cmd.Parameters.Add(param);
+ return param;
+ }
+ /// <summary>
+ /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Input"/> with the specified value
+ /// and adds it to the command.
+ /// </summary>
+ /// <param name="cmd"></param>
+ /// <param name="name">The parameter name</param>
+ /// <param name="value">The value of the parameter</param>
+ /// <param name="type">The <see cref="DbType"/> of the column</param>
+ /// <param name="size">Size of the data value</param>
+ /// <param name="nullable">Are null types allowed in the value parameter</param>
+ /// <returns>The created parameter</returns>
+ public static DbParameter AddParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, int @size, bool @nullable = false)
+ {
+ DbParameter param = AddParameter(cmd, name, value, type, nullable);
+ //Set size parameter
+ param.Size = size;
+ return param;
+ }
+ /// <summary>
+ /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Output"/> with the specified value
+ /// and adds it to the command.
+ /// </summary>
+ /// <param name="cmd"></param>
+ /// <param name="name">The parameter name</param>
+ /// <param name="value">The value of the parameter</param>
+ /// <param name="type">The <see cref="DbType"/> of the column</param>
+ /// <param name="nullable">Are null types allowed in the value parameter</param>
+ /// <returns>The created parameter</returns>
+ public static DbParameter AddOutParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, bool @nullable = false)
+ {
+ //Create the new parameter from command
+ DbParameter param = AddParameter(cmd, name, value, type, nullable);
+ //Specify output parameter
+ param.Direction = ParameterDirection.Output;
+ return param;
+ }
+ /// <summary>
+ /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Output"/> with the specified value
+ /// and adds it to the command.
+ /// </summary>
+ /// <param name="cmd"></param>
+ /// <param name="name">The parameter name</param>
+ /// <param name="value">The value of the parameter</param>
+ /// <param name="type">The <see cref="DbType"/> of the column</param>
+ /// <param name="size">Size of the data value</param>
+ /// <param name="nullable">Are null types allowed in the value parameter</param>
+ /// <returns>The created parameter</returns>
+ public static DbParameter AddOutParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, int @size, bool @nullable = false)
+ {
+ DbParameter param = AddOutParameter(cmd, name, value, type, nullable);
+ //Set size parameter
+ param.Size = size;
+ return param;
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.Text"/> with the specified command
+ /// </summary>
+ /// <param name="db"></param>
+ /// <param name="cmdText">The command to run against the connection</param>
+ /// <returns>The initalized <see cref="DbCommand"/></returns>
+ public static DbCommand CreateTextCommand(this DbConnection db, string cmdText)
+ {
+ //Create the new command
+ DbCommand cmd = db.CreateCommand();
+ cmd.CommandText = cmdText;
+ cmd.CommandType = CommandType.Text; //Specify text command
+ return cmd;
+ }
+ /// <summary>
+ /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.StoredProcedure"/> with the specified procedure name
+ /// </summary>
+ /// <param name="db"></param>
+ /// <param name="procedureName">The name of the stored proecedure to execute</param>
+ /// <returns>The initalized <see cref="DbCommand"/></returns>
+ public static DbCommand CreateProcedureCommand(this DbConnection db, string procedureName)
+ {
+ //Create the new command
+ DbCommand cmd = db.CreateCommand();
+ cmd.CommandText = procedureName;
+ cmd.CommandType = CommandType.StoredProcedure; //Specify stored procedure
+ return cmd;
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.Text"/> with the specified command
+ /// on a given transaction
+ /// </summary>
+ /// <param name="db"></param>
+ /// <param name="cmdText">The command to run against the connection</param>
+ /// <param name="transaction">The transaction to execute on</param>
+ /// <returns>The initalized <see cref="DbCommand"/></returns>
+ public static DbCommand CreateTextCommand(this DbConnection db, string cmdText, DbTransaction transaction)
+ {
+ return CreateCommand(db, transaction, CommandType.Text, cmdText);
+ }
+ /// <summary>
+ /// Shortcut to create a command on a transaction with the specifed command type and command
+ /// </summary>
+ /// <param name="db"></param>
+ /// <param name="transaction">The transaction to complete the operation on</param>
+ /// <param name="type">The command type</param>
+ /// <param name="command">The command to execute</param>
+ /// <returns>The intialized db command</returns>
+ public static DbCommand CreateCommand(this DbConnection db, DbTransaction transaction, CommandType type, string command)
+ {
+ //Create the new command
+ DbCommand cmd = db.CreateCommand();
+ cmd.Transaction = transaction;
+ cmd.CommandText = command;
+ cmd.CommandType = type;
+ return cmd;
+ }
+ /// <summary>
+ /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.StoredProcedure"/> with the specified procedure name
+ /// </summary>
+ /// <param name="db"></param>
+ /// <param name="procedureName">The name of the stored proecedure to execute</param>
+ /// <param name="transaction">The transaction to execute on</param>
+ /// <returns>The initalized <see cref="DbCommand"/></returns>
+ public static DbCommand CreateProcedureCommand(this DbConnection db, string procedureName, DbTransaction transaction)
+ {
+ return CreateCommand(db, transaction, CommandType.StoredProcedure, procedureName);
+ }
+
+ /// <summary>
+ /// Reads all available rows from the reader, adapts columns to public properties with <see cref="SqlColumnName"/>
+ /// attributes, and adds them to the collection
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="reader"></param>
+ /// <param name="container">The container to write created objects to</param>
+ /// <returns>The number of objects created and written to the collection</returns>
+ public static int GetAllObjects<T>(this DbDataReader reader, ICollection<T> container) where T : new()
+ {
+ //make sure its worth collecting object meta
+ if (!reader.HasRows)
+ {
+ return 0;
+ }
+ Type objectType = typeof(T);
+ //Rent a dict of properties that have the column attribute set so we can load the proper results
+ Dictionary<string, PropertyInfo> avialbleProps = DictStore.Rent();
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objectType.GetProperties())
+ {
+ //try to get the column name attribute of the propery
+ SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute<SqlColumnNameAttribute>(true);
+ //Attribute is valid and coumn name is not empty
+ if (!string.IsNullOrWhiteSpace(colAtt?.ColumnName))
+ {
+ //Store the property for later
+ avialbleProps[colAtt.ColumnName] = prop;
+ }
+ }
+ //Get the column schema
+ ReadOnlyCollection<DbColumn> columns = reader.GetColumnSchema();
+ int count = 0;
+ //Read
+ while (reader.Read())
+ {
+ //Create the new object
+ T ret = new();
+ //Iterate through columns
+ foreach (DbColumn col in columns)
+ {
+ //Get the propery if its specified by its column-name attribute
+ if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop))
+ {
+ //make sure the column has a value
+ if (col.ColumnOrdinal.HasValue)
+ {
+ //Get the object
+ object val = reader.GetValue(col.ColumnOrdinal.Value);
+ //Set check if the row is DB null, if so set it, otherwise set the value
+ prop.SetValue(ret, Convert.IsDBNull(val) ? null : val);
+ }
+ }
+ }
+ //Add the object to the collection
+ container.Add(ret);
+ //Increment count
+ count++;
+ }
+ //return dict (if an error occurs, just let the dict go and create a new one next time, no stress setting up a try/finally block)
+ DictStore.Return(avialbleProps);
+ return count;
+ }
+ /// <summary>
+ /// Reads all available rows from the reader, adapts columns to public properties with <see cref="SqlColumnName"/>
+ /// attributes, and adds them to the collection
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="reader"></param>
+ /// <param name="container">The container to write created objects to</param>
+ /// <returns>The number of objects created and written to the collection</returns>
+ public static async ValueTask<int> GetAllObjectsAsync<T>(this DbDataReader reader, ICollection<T> container) where T : new()
+ {
+ //make sure its worth collecting object meta
+ if (!reader.HasRows)
+ {
+ return 0;
+ }
+ Type objectType = typeof(T);
+ //Rent a dict of properties that have the column attribute set so we can load the proper results
+ Dictionary<string, PropertyInfo> avialbleProps = DictStore.Rent();
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objectType.GetProperties())
+ {
+ //try to get the column name attribute of the propery
+ SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute<SqlColumnNameAttribute>(true);
+ //Attribute is valid and coumn name is not empty
+ if (!string.IsNullOrWhiteSpace(colAtt?.ColumnName))
+ {
+ //Store the property for later
+ avialbleProps[colAtt.ColumnName] = prop;
+ }
+ }
+ //Get the column schema
+ ReadOnlyCollection<DbColumn> columns = await reader.GetColumnSchemaAsync();
+ int count = 0;
+ //Read
+ while (await reader.ReadAsync())
+ {
+ //Create the new object
+ T ret = new();
+ //Iterate through columns
+ foreach (DbColumn col in columns)
+ {
+ //Get the propery if its specified by its column-name attribute
+ if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop))
+ {
+ //make sure the column has a value
+ if (col.ColumnOrdinal.HasValue)
+ {
+ //Get the object
+ object val = reader.GetValue(col.ColumnOrdinal.Value);
+ //Set check if the row is DB null, if so set it, otherwise set the value
+ prop.SetValue(ret, Convert.IsDBNull(val) ? null : val);
+ }
+ }
+ }
+ //Add the object to the collection
+ container.Add(ret);
+ //Increment count
+ count++;
+ }
+ //return dict (if an error occurs, just let the dict go and create a new one next time, no stress setting up a try/finally block)
+ DictStore.Return(avialbleProps);
+ return count;
+ }
+ /// <summary>
+ /// Reads the first available row from the reader, adapts columns to public properties with <see cref="SqlColumnName"/>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="reader"></param>
+ /// <returns>The created object, or default if no rows are available</returns>
+ public static T? GetFirstObject<T>(this DbDataReader reader) where T : new()
+ {
+ //make sure its worth collecting object meta
+ if (!reader.HasRows)
+ {
+ return default;
+ }
+ //Get the object type
+ Type objectType = typeof(T);
+ //Get the column schema
+ ReadOnlyCollection<DbColumn> columns = reader.GetColumnSchema();
+ //Read
+ if (reader.Read())
+ {
+ //Rent a dict of properties that have the column attribute set so we can load the proper results
+ Dictionary<string, PropertyInfo> availbleProps = DictStore.Rent();
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objectType.GetProperties())
+ {
+ //try to get the column name attribute of the propery
+ SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute<SqlColumnNameAttribute>(true);
+ //Attribute is valid and coumn name is not empty
+ if (colAtt != null && !string.IsNullOrWhiteSpace(colAtt.ColumnName))
+ {
+ //Store the property for later
+ availbleProps[colAtt.ColumnName] = prop;
+ }
+ }
+ //Create the new object
+ T ret = new();
+ //Iterate through columns
+ foreach (DbColumn col in columns)
+ {
+ //Get the propery if its specified by its column-name attribute
+ if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop) && col.ColumnOrdinal.HasValue)
+ {
+ //Get the object
+ object val = reader.GetValue(col.ColumnOrdinal.Value);
+ //Set check if the row is DB null, if so set it, otherwise set the value
+ prop.SetValue(ret, Convert.IsDBNull(val) ? null : val);
+ }
+ }
+ //Return dict, no stress if error occurs, the goal is lower overhead
+ DictStore.Return(availbleProps);
+ //Return the new object
+ return ret;
+ }
+ return default;
+ }
+ /// <summary>
+ /// Reads the first available row from the reader, adapts columns to public properties with <see cref="SqlColumnName"/>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="reader"></param>
+ /// <returns>The created object, or default if no rows are available</returns>
+ public static async Task<T?> GetFirstObjectAsync<T>(this DbDataReader reader) where T : new()
+ {
+ //Read
+ if (await reader.ReadAsync())
+ {
+ //Get the object type
+ Type objectType = typeof(T);
+ //Get the column schema
+ ReadOnlyCollection<DbColumn> columns = await reader.GetColumnSchemaAsync();
+ //Rent a dict of properties that have the column attribute set so we can load the proper results
+ Dictionary<string, PropertyInfo> availbleProps = DictStore.Rent();
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objectType.GetProperties())
+ {
+ //try to get the column name attribute of the propery
+ SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute<SqlColumnNameAttribute>(true);
+ //Attribute is valid and coumn name is not empty
+ if (colAtt != null && !string.IsNullOrWhiteSpace(colAtt.ColumnName))
+ {
+ //Store the property for later
+ availbleProps[colAtt.ColumnName] = prop;
+ }
+ }
+ //Create the new object
+ T ret = new();
+ //Iterate through columns
+ foreach (DbColumn col in columns)
+ {
+ //Get the propery if its specified by its column-name attribute
+ if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop) && col.ColumnOrdinal.HasValue)
+ {
+ //Get the object
+ object val = reader.GetValue(col.ColumnOrdinal.Value);
+ //Set check if the row is DB null, if so set it, otherwise set the value
+ prop.SetValue(ret, Convert.IsDBNull(val) ? null : val);
+ }
+ }
+ //Return dict, no stress if error occurs, the goal is lower overhead
+ DictStore.Return(availbleProps);
+ //Return the new object
+ return ret;
+ }
+ return default;
+ }
+ /// <summary>
+ /// Executes a nonquery operation with the specified command using the object properties set with the
+ /// <see cref="SqlVariableAttribute"/> attributes
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="cmd"></param>
+ /// <param name="obj">The object containing the <see cref="SqlVariableAttribute"/> properties to write to command variables</param>
+ /// <returns>The number of rows affected</returns>
+ /// <exception cref="TypeLoadException"></exception>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="AmbiguousMatchException"></exception>
+ /// <exception cref="TargetInvocationException"></exception>
+ public static ERRNO ExecuteNonQuery<T>(this DbCommand cmd, T obj) where T : notnull
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+ //Get the objec type
+ Type objtype = typeof(T);
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objtype.GetProperties())
+ {
+ //try to get the variable attribute of the propery
+ SqlVariableAttribute varprops = prop.GetCustomAttribute<SqlVariableAttribute>(true);
+ //This property is an sql variable, so lets add it
+ if (varprops == null)
+ {
+ continue;
+ }
+ //If the command type is text, then make sure the variable is actually in the command, if not, ignore it
+ if (cmd.CommandType != CommandType.Text || cmd.CommandText.Contains(varprops.VariableName))
+ {
+ //Add the parameter to the command list
+ cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.IsNullable).Direction = varprops.Direction;
+ }
+ }
+ //Prepare the sql statement
+ cmd.Prepare();
+ //Exect the query and return the results
+ return cmd.ExecuteNonQuery();
+ }
+ /// <summary>
+ /// Executes a nonquery operation with the specified command using the object properties set with the
+ /// <see cref="SqlVariable"/> attributes
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="cmd"></param>
+ /// <param name="obj">The object containing the <see cref="SqlVariable"/> properties to write to command variables</param>
+ /// <returns>The number of rows affected</returns>
+ /// <exception cref="TypeLoadException"></exception>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="AmbiguousMatchException"></exception>
+ /// <exception cref="TargetInvocationException"></exception>
+ public static async Task<ERRNO> ExecuteNonQueryAsync<T>(this DbCommand cmd, T obj) where T : notnull
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+ //Get the objec type
+ Type objtype = typeof(T);
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objtype.GetProperties())
+ {
+ //try to get the variable attribute of the propery
+ SqlVariableAttribute? varprops = prop.GetCustomAttribute<SqlVariableAttribute>(true);
+ //This property is an sql variable, so lets add it
+ if (varprops == null)
+ {
+ continue;
+ }
+ //If the command type is text, then make sure the variable is actually in the command, if not, ignore it
+ if (cmd.CommandType != CommandType.Text || cmd.CommandText.Contains(varprops.VariableName))
+ {
+ //Add the parameter to the command list
+ cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.IsNullable).Direction = varprops.Direction;
+ }
+ }
+ //Prepare the sql statement
+ await cmd.PrepareAsync();
+ //Exect the query and return the results
+ return await cmd.ExecuteNonQueryAsync();
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/EnumerableTable.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/EnumerableTable.cs
new file mode 100644
index 0000000..23cd889
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/SQL/EnumerableTable.cs
@@ -0,0 +1,118 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: EnumerableTable.cs
+*
+* EnumerableTable.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Data;
+using System.Threading;
+using System.Data.Common;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+namespace VNLib.Plugins.Extensions.Data.SQL
+{
+ /// <summary>
+ /// A base class for client side async enumerable SQL queries
+ /// </summary>
+ /// <typeparam name="T">The entity type</typeparam>
+ public abstract class EnumerableTable<T> : TableManager, IAsyncEnumerable<T>
+ {
+ const string DEFAULT_ENUM_STATMENT = "SELECT *\r\nFROM @table\r\n;";
+
+ public EnumerableTable(Func<DbConnection> factory, string tableName) : base(factory, tableName)
+ {
+ //Build the default select all statment
+ Enumerate = DEFAULT_ENUM_STATMENT.Replace("@table", tableName);
+ }
+ public EnumerableTable(Func<DbConnection> factory) : base(factory)
+ { }
+
+ /// <summary>
+ /// The command that will be run against the database to return rows for enumeration
+ /// </summary>
+ protected string Enumerate { get; set; }
+
+ /// <summary>
+ /// The isolation level to use when creating the transaction during enumerations
+ /// </summary>
+ protected IsolationLevel TransactionIsolationLevel { get; set; } = IsolationLevel.ReadUncommitted;
+
+ IAsyncEnumerator<T> IAsyncEnumerable<T>.GetAsyncEnumerator(CancellationToken cancellationToken)
+ {
+ return GetAsyncEnumerator(cancellationToken: cancellationToken);
+ }
+
+ /// <summary>
+ /// Transforms a row from the <paramref name="reader"/> into the item type
+ /// to be returned when yielded.
+ /// </summary>
+ /// <param name="reader">The reader to get the item data from</param>
+ /// <param name="cancellationToken">A token to cancel the operation</param>
+ /// <returns>A task that returns the transformed item</returns>
+ /// <remarks>The <paramref name="reader"/> position is set before this method is invoked</remarks>
+ protected abstract Task<T> GetItemAsync(DbDataReader reader, CancellationToken cancellationToken);
+ /// <summary>
+ /// Invoked when an item is no longer in the enumerator scope, in the enumeration process.
+ /// </summary>
+ /// <param name="item">The item to cleanup</param>
+ /// <param name="cancellationToken">A token to cancel the operation</param>
+ /// <returns>A ValueTask that represents the cleanup process</returns>
+ protected abstract ValueTask CleanupItemAsync(T item, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets an <see cref="IAsyncEnumerator{T}"/> to enumerate items within the backing store.
+ /// </summary>
+ /// <param name="closeItems">Cleanup items after each item is enumerated and the enumeration scope has
+ /// returned to the enumerator</param>
+ /// <param name="cancellationToken">A token to cancel the enumeration</param>
+ /// <returns>A <see cref="IAsyncEnumerator{T}"/> to enumerate records within the store</returns>
+ public virtual async IAsyncEnumerator<T> GetAsyncEnumerator(bool closeItems = true, CancellationToken cancellationToken = default)
+ {
+ await using DbConnection db = GetConnection();
+ await db.OpenAsync(cancellationToken);
+ await using DbTransaction transaction = await db.BeginTransactionAsync(cancellationToken);
+ //Start the enumeration command
+ await using DbCommand cmd = db.CreateTextCommand(Enumerate, transaction);
+ await cmd.PrepareAsync(cancellationToken);
+ await using DbDataReader reader = await cmd.ExecuteReaderAsync(cancellationToken);
+ //loop through results and transform each element
+ while (reader.Read())
+ {
+ //get the item
+ T item = await GetItemAsync(reader, cancellationToken);
+ try
+ {
+ yield return item;
+ }
+ finally
+ {
+ if (closeItems)
+ {
+ //Cleanup the item
+ await CleanupItemAsync(item, cancellationToken);
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlColumnNameAttribute.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlColumnNameAttribute.cs
new file mode 100644
index 0000000..c18dab9
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlColumnNameAttribute.cs
@@ -0,0 +1,54 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: SqlColumnNameAttribute.cs
+*
+* SqlColumnNameAttribute.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Data.SQL
+{
+ /// <summary>
+ /// Property attribute that specifies the property represents an SQL column in the database
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class SqlColumnNameAttribute : Attribute
+ {
+ public bool Nullable { get; }
+ public bool Unique { get; }
+ public bool PrimaryKey { get; }
+ public string ColumnName { get; }
+ /// <summary>
+ /// Specifies the property is an SQL column name
+ /// </summary>
+ /// <param name="columnName">Name of the SQL column</param>
+ /// <param name="primaryKey"></param>
+ /// <param name="nullable"></param>
+ /// <param name="unique"></param>
+ public SqlColumnNameAttribute(string columnName, bool primaryKey = false, bool nullable = true, bool unique = false)
+ {
+ this.ColumnName = columnName;
+ this.PrimaryKey = primaryKey;
+ this.Nullable = nullable;
+ this.Unique = unique;
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlTableNameAttribute.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlTableNameAttribute.cs
new file mode 100644
index 0000000..9c870ea
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlTableNameAttribute.cs
@@ -0,0 +1,40 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: SqlColumnName.cs
+*
+* SqlColumnName.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Data.SQL
+{
+
+ /// <summary>
+ /// Allows a type to declare itself as a <see cref="System.Data.DataTable"/> with the specified name
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple =false, Inherited = true)]
+ public sealed class SqlTableNameAttribute : Attribute
+ {
+ public string TableName { get; }
+
+ public SqlTableNameAttribute(string tableName) => TableName = tableName;
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlVariable.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlVariable.cs
new file mode 100644
index 0000000..b18d27b
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlVariable.cs
@@ -0,0 +1,58 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: SqlVariable.cs
+*
+* SqlVariable.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Data;
+
+namespace VNLib.Plugins.Extensions.Data.SQL
+{
+ /// <summary>
+ /// Property attribute that specifies the property is to be used for a given command variable
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+ public sealed class SqlVariableAttribute : Attribute
+ {
+ public string VariableName { get; }
+ public DbType DataType { get; }
+ public ParameterDirection Direction { get; }
+ public int Size { get; }
+ public bool IsNullable { get; }
+ /// <summary>
+ /// Specifies the property to be used as an SQL variable
+ /// </summary>
+ /// <param name="variableName">Sql statement variable this property will substitute</param>
+ /// <param name="dataType">The sql data the property will represent</param>
+ /// <param name="direction">Data direction during execution</param>
+ /// <param name="size">Column size</param>
+ /// <param name="isNullable">Is this property allowed to be null</param>
+ public SqlVariableAttribute(string variableName, DbType dataType, ParameterDirection direction, int size, bool isNullable)
+ {
+ this.VariableName = variableName;
+ this.DataType = dataType;
+ this.Direction = direction;
+ this.Size = size;
+ this.IsNullable = isNullable;
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/TableManager.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/TableManager.cs
new file mode 100644
index 0000000..a7f7873
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/SQL/TableManager.cs
@@ -0,0 +1,66 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: TableManager.cs
+*
+* TableManager.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Data.Common;
+
+namespace VNLib.Plugins.Extensions.Data.SQL
+{
+ /// <summary>
+ /// A class that contains basic structures for interacting with an SQL driven database
+ /// </summary>
+ public abstract class TableManager
+ {
+ private readonly Func<DbConnection> Factory;
+ protected string Insert { get; set; }
+ protected string Select { get; set; }
+ protected string Update { get; set; }
+ protected string Delete { get; set; }
+
+ /// <summary>
+ /// The name of the table specified during initialized
+ /// </summary>
+ protected string TableName { get; }
+
+ protected TableManager(Func<DbConnection> factory, string tableName)
+ {
+ this.Factory = factory ?? throw new ArgumentNullException(nameof(factory));
+ this.TableName = !string.IsNullOrWhiteSpace(tableName) ? tableName : throw new ArgumentNullException(nameof(tableName));
+ }
+
+ protected TableManager(Func<DbConnection> factory)
+ {
+ this.Factory = factory ?? throw new ArgumentNullException(nameof(factory));
+ this.TableName = "";
+ }
+ /// <summary>
+ /// Opens a new <see cref="DbConnection"/> by invoking the factory callback method
+ /// </summary>
+ /// <returns>The open connection</returns>
+ protected DbConnection GetConnection()
+ {
+ return Factory();
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/Blob.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Blob.cs
new file mode 100644
index 0000000..ab18eeb
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Blob.cs
@@ -0,0 +1,244 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: Blob.cs
+*
+* Blob.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using System.Runtime.Versioning;
+
+using VNLib.Utils;
+using VNLib.Utils.IO;
+using VNLib.Utils.Async;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+ /// <summary>
+ /// Represents a stream of arbitrary binary data
+ /// </summary>
+ public class Blob : BackingStream<FileStream>, IObjectStorage, IAsyncExclusiveResource
+ {
+ protected readonly LWStorageDescriptor Descriptor;
+
+ /// <summary>
+ /// The current blob's unique ID
+ /// </summary>
+ public string BlobId => Descriptor.DescriptorID;
+ /// <summary>
+ /// A value indicating if the <see cref="Blob"/> has been modified
+ /// </summary>
+ public bool Modified { get; protected set; }
+ /// <summary>
+ /// A valid indicating if the blob was flagged for deletiong
+ /// </summary>
+ public bool Deleted { get; protected set; }
+
+ /// <summary>
+ /// The name of the file (does not change the actual file system name)
+ /// </summary>
+ public string Name
+ {
+ get => Descriptor.GetName();
+ set => Descriptor.SetName(value);
+ }
+ /// <summary>
+ /// The UTC time the <see cref="Blob"/> was last modified
+ /// </summary>
+ public DateTimeOffset LastWriteTimeUtc => Descriptor.LastModified;
+ /// <summary>
+ /// The UTC time the <see cref="Blob"/> was created
+ /// </summary>
+ public DateTimeOffset CreationTimeUtc => Descriptor.Created;
+
+ internal Blob(LWStorageDescriptor descriptor, in FileStream file)
+ {
+ this.Descriptor = descriptor;
+ base.BaseStream = file;
+ }
+
+ /// <summary>
+ /// Prevents other processes from reading from or writing to the <see cref="Blob"/>
+ /// </summary>
+ /// <param name="position">The begining position of the range to lock</param>
+ /// <param name="length">The range to be locked</param>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [UnsupportedOSPlatform("ios")]
+ [UnsupportedOSPlatform("macos")]
+ [UnsupportedOSPlatform("tvos")]
+ public void Lock(long position, long length) => BaseStream.Lock(position, length);
+ /// <summary>
+ /// Prevents other processes from reading from or writing to the <see cref="Blob"/>
+ /// </summary>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [UnsupportedOSPlatform("ios")]
+ [UnsupportedOSPlatform("macos")]
+ [UnsupportedOSPlatform("tvos")]
+ public void Lock() => BaseStream.Lock(0, BaseStream.Length);
+ /// <summary>
+ /// Allows access by other processes to all or part of the <see cref="Blob"/> that was previously locked
+ /// </summary>
+ /// <param name="position">The begining position of the range to unlock</param>
+ /// <param name="length">The range to be unlocked</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [UnsupportedOSPlatform("ios")]
+ [UnsupportedOSPlatform("macos")]
+ [UnsupportedOSPlatform("tvos")]
+ public void Unlock(long position, long length) => BaseStream.Unlock(position, length);
+ /// <summary>
+ /// Allows access by other processes to the entire <see cref="Blob"/>
+ /// </summary>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [UnsupportedOSPlatform("ios")]
+ [UnsupportedOSPlatform("macos")]
+ [UnsupportedOSPlatform("tvos")]
+ public void Unlock() => BaseStream.Unlock(0, BaseStream.Length);
+ ///<inheritdoc/>
+ public override void SetLength(long value)
+ {
+ base.SetLength(value);
+ //Set modified flag
+ Modified |= true;
+ }
+
+ /*
+ * Capture on-write calls to set the modified flag
+ */
+ ///<inheritdoc/>
+ protected override void OnWrite(int count) => Modified |= true;
+
+ T IObjectStorage.GetObject<T>(string key) => ((IObjectStorage)Descriptor).GetObject<T>(key);
+ void IObjectStorage.SetObject<T>(string key, T obj) => ((IObjectStorage)Descriptor).SetObject(key, obj);
+
+ public string this[string index]
+ {
+ get => Descriptor[index];
+ set => Descriptor[index] = value;
+ }
+
+
+ /// <summary>
+ /// Marks the file for deletion and will be deleted when the <see cref="Blob"/> is disposed
+ /// </summary>
+ public void Delete()
+ {
+ //Set deleted flag
+ Deleted |= true;
+ Descriptor.Delete();
+ }
+ ///<inheritdoc/>
+ public bool IsReleased => Descriptor.IsReleased;
+
+
+ /// <summary>
+ /// <para>
+ /// If the <see cref="Blob"/> was opened with writing enabled,
+ /// and file was modified, changes are flushed to the backing store
+ /// and the stream is set to readonly.
+ /// </para>
+ /// <para>
+ /// If calls to this method succeed the stream is placed into a read-only mode
+ /// which will cause any calls to Write to throw a <see cref="NotSupportedException"/>
+ /// </para>
+ /// </summary>
+ /// <returns>A <see cref="ValueTask"/> that may be awaited until the operation completes</returns>
+ /// <remarks>
+ /// This method may be called to avoid flushing changes to the backing store
+ /// when the <see cref="Blob"/> is disposed (i.e. lifetime is manged outside of the desired scope)
+ /// </remarks>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ public async ValueTask FlushChangesAndSetReadonlyAsync()
+ {
+ if (Deleted)
+ {
+ throw new InvalidOperationException("The blob has been deleted and must be closed!");
+ }
+ if (Modified)
+ {
+ //flush the base stream
+ await BaseStream.FlushAsync();
+ //Update the file length in the store
+ Descriptor.SetLength(BaseStream.Length);
+ }
+ //flush changes, this will cause the dispose method to complete synchronously when closing
+ await Descriptor.WritePendingChangesAsync();
+ //Clear modified flag
+ Modified = false;
+ //Set to readonly mode
+ base.ForceReadOnly = true;
+ }
+
+
+ /*
+ * Override the dispose async to manually dispose the
+ * base stream and avoid the syncrhonous (OnClose)
+ * method and allow awaiting the descriptor release
+ */
+ ///<inheritdoc/>
+ public override async ValueTask DisposeAsync()
+ {
+ await ReleaseAsync();
+ GC.SuppressFinalize(this);
+ }
+ ///<inheritdoc/>
+ public async ValueTask ReleaseAsync()
+ {
+ try
+ {
+ //Check for deleted
+ if (Deleted)
+ {
+ //Dispose the base stream explicitly
+ await BaseStream.DisposeAsync();
+ //Try to delete the file
+ File.Delete(BaseStream.Name);
+ }
+ //Check to see if the file was modified
+ else if (Modified)
+ {
+ //Set the file size in bytes
+ Descriptor.SetLength(BaseStream.Length);
+ }
+ }
+ catch
+ {
+ //Set the error flag
+ Descriptor.IsError(true);
+ //propagate the exception
+ throw;
+ }
+ finally
+ {
+ //Dispose the stream
+ await BaseStream.DisposeAsync();
+ //Release the descriptor
+ await Descriptor.ReleaseAsync();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobExtensions.cs
new file mode 100644
index 0000000..468a66d
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobExtensions.cs
@@ -0,0 +1,67 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: BlobExtensions.cs
+*
+* BlobExtensions.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+using VNLib.Utils;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+ public static class BlobExtensions
+ {
+ public const string USER_ID_ENTRY = "__.uid";
+ public const string VERSION_ENTRY = "__.vers";
+
+ private const string FILE_SIZE = "__.size";
+ private const string FILE_NAME = "__.name";
+ private const string ERROR_FLAG = "__.err";
+
+ public static string GetUserId(this Blob blob) => blob[USER_ID_ENTRY];
+ /// <summary>
+ /// Gets the <see cref="Version"/> stored in the current <see cref="Blob"/>
+ /// </summary>
+ /// <returns>The sored version if previously set, thows otherwise</returns>
+ /// <exception cref="FormatException"></exception>
+ public static Version GetVersion(this Blob blob) => Version.Parse(blob[VERSION_ENTRY]);
+ /// <summary>
+ /// Sets a <see cref="Version"/> for the current <see cref="Blob"/>
+ /// </summary>
+ /// <param name="blob"></param>
+ /// <param name="version">The <see cref="Version"/> of the <see cref="Blob"/></param>
+ public static void SetVersion(this Blob blob, Version version) => blob[VERSION_ENTRY] = version.ToString();
+
+ /// <summary>
+ /// Gets a value indicating if the last operation left the <see cref="Blob"/> in an undefined state
+ /// </summary>
+ /// <returns>True if the <see cref="Blob"/> state is undefined, false otherwise</returns>
+ public static bool IsError(this Blob blob) => bool.TrueString.Equals(blob[ERROR_FLAG]);
+ internal static void IsError(this LWStorageDescriptor blob, bool value) => blob[ERROR_FLAG] = value ? bool.TrueString : null;
+
+ internal static long GetLength(this LWStorageDescriptor blob) => (blob as IObjectStorage).GetObject<long>(FILE_SIZE);
+ internal static void SetLength(this LWStorageDescriptor blob, long length) => (blob as IObjectStorage).SetObject(FILE_SIZE, length);
+
+ internal static string GetName(this LWStorageDescriptor blob) => blob[FILE_NAME];
+ internal static string SetName(this LWStorageDescriptor blob, string filename) => blob[FILE_NAME] = filename;
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobStore.cs
new file mode 100644
index 0000000..6897516
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobStore.cs
@@ -0,0 +1,162 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: BlobStore.cs
+*
+* BlobStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Threading.Tasks;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+
+ /// <summary>
+ /// Stores <see cref="Blob"/>s to the local file system backed with a single table <see cref="LWStorageManager"/>
+ /// that tracks changes
+ /// </summary>
+ public class BlobStore
+ {
+ /// <summary>
+ /// The root directory all blob files are stored
+ /// </summary>
+ public DirectoryInfo RootDir { get; }
+ /// <summary>
+ /// The backing store for blob meta-data
+ /// </summary>
+ protected LWStorageManager BlobTable { get; }
+ /// <summary>
+ /// Creates a new <see cref="BlobStore"/> that accesses files
+ /// within the specified root directory.
+ /// </summary>
+ /// <param name="rootDir">The root directory containing the blob file contents</param>
+ /// <param name="blobStoreMan">The db backing store</param>
+ public BlobStore(DirectoryInfo rootDir, LWStorageManager blobStoreMan)
+ {
+ RootDir = rootDir;
+ BlobTable = blobStoreMan;
+ }
+
+ private string GetPath(string fileId) => Path.Combine(RootDir.FullName, fileId);
+
+ /*
+ * Creates a repeatable unique identifier for the file
+ * name and allows for lookups
+ */
+ internal static string CreateFileHash(string fileName)
+ {
+ throw new NotImplementedException();
+ //return ManagedHash.ComputeBase64Hash(fileName, HashAlg.SHA1);
+ }
+
+ /// <summary>
+ /// Opens an existing <see cref="Blob"/> from the current store
+ /// </summary>
+ /// <param name="fileId">The id of the file being requested</param>
+ /// <param name="access">Access level of the file</param>
+ /// <param name="share">The sharing option of the underlying file</param>
+ /// <param name="bufferSize">The size of the file buffer</param>
+ /// <returns>If found, the requested <see cref="Blob"/>, null otherwise. Throws exceptions if the file is opened in a non-sharable state</returns>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="UnauthorizedAccessException"></exception>
+ /// <exception cref="UndefinedBlobStateException"></exception>
+ public virtual async Task<Blob> OpenBlobAsync(string fileId, FileAccess access, FileShare share, int bufferSize = 4096)
+ {
+ //Get the file's data descriptor
+ LWStorageDescriptor fileDescriptor = await BlobTable.GetDescriptorFromIDAsync(fileId);
+ //return null if not found
+ if (fileDescriptor == null)
+ {
+ return null;
+ }
+ try
+ {
+ string fsSafeName = GetPath(fileDescriptor.DescriptorID);
+ //try to open the file
+ FileStream file = new(fsSafeName, FileMode.Open, access, share, bufferSize, FileOptions.Asynchronous);
+ //Create the new blob
+ return new Blob(fileDescriptor, file);
+ }
+ catch (FileNotFoundException)
+ {
+ //If the file was not found but the descriptor was, delete the descriptor from the db
+ fileDescriptor.Delete();
+ //Flush changes
+ await fileDescriptor.ReleaseAsync();
+ //return null since this is a desync issue and the file technically does not exist
+ return null;
+ }
+ catch
+ {
+ //Release the descriptor and pass the exception
+ await fileDescriptor.ReleaseAsync();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="Blob"/> for the specified file sharing permissions
+ /// </summary>
+ /// <param name="name">The name of the original file</param>
+ /// <param name="share">The blob sharing permissions</param>
+ /// <param name="bufferSize"></param>
+ /// <returns>The newly created <see cref="Blob"/></returns>
+ /// <exception cref="IoExtensions"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="UnauthorizedAccessException"></exception>
+ public virtual async Task<Blob> CreateBlobAsync(string name, FileShare share = FileShare.None, int bufferSize = 4096)
+ {
+ //hash the file name to create a unique id for the file name
+ LWStorageDescriptor newFile = await BlobTable.CreateDescriptorAsync(CreateFileHash(name));
+ //if the descriptor was not created, return null
+ if (newFile == null)
+ {
+ return null;
+ }
+ try
+ {
+ string fsSafeName = GetPath(newFile.DescriptorID);
+ //Open/create the new file
+ FileStream file = new(fsSafeName, FileMode.OpenOrCreate, FileAccess.ReadWrite, share, bufferSize, FileOptions.Asynchronous);
+ //If the file already exists, make sure its zero'd
+ file.SetLength(0);
+ //Save the original name of the file
+ newFile.SetName(name);
+ //Create and return the new blob
+ return new Blob(newFile, file);
+ }
+ catch
+ {
+ //If an exception occurs, remove the descritor from the db
+ newFile.Delete();
+ await newFile.ReleaseAsync();
+ //Pass exception
+ throw;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWDecriptorCreationException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWDecriptorCreationException.cs
new file mode 100644
index 0000000..db0dbbb
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWDecriptorCreationException.cs
@@ -0,0 +1,45 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: LWDecriptorCreationException.cs
+*
+* LWDecriptorCreationException.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+ /// <summary>
+ /// Raised when an operation to create a new <see cref="LWStorageDescriptor"/>
+ /// fails
+ /// </summary>
+ public class LWDescriptorCreationException : Exception
+ {
+ ///<inheritdoc/>
+ public LWDescriptorCreationException()
+ {}
+ ///<inheritdoc/>
+ public LWDescriptorCreationException(string? message) : base(message)
+ {}
+ ///<inheritdoc/>
+ public LWDescriptorCreationException(string? message, Exception? innerException) : base(message, innerException)
+ {}
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs
new file mode 100644
index 0000000..d7f6e29
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs
@@ -0,0 +1,48 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: LWStorageContext.cs
+*
+* LWStorageContext.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using Microsoft.EntityFrameworkCore;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+#nullable disable
+ internal sealed class LWStorageContext : TransactionalDbContext
+ {
+ private readonly string TableName;
+ public DbSet<LWStorageEntry> Descriptors { get; set; }
+
+ public LWStorageContext(DbContextOptions options, string tableName)
+ :base(options)
+ {
+ TableName = tableName;
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ //Set table name
+ modelBuilder.Entity<LWStorageEntry>()
+ .ToTable(TableName);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs
new file mode 100644
index 0000000..72665f3
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs
@@ -0,0 +1,230 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: LWStorageDescriptor.cs
+*
+* LWStorageDescriptor.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Collections;
+using System.IO.Compression;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+using VNLib.Utils;
+using VNLib.Utils.Async;
+using VNLib.Utils.Extensions;
+using VNLib.Utils.Memory;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+ /// <summary>
+ /// Represents an open storage object, that when released or disposed, will flush its changes to the underlying table
+ /// for which this descriptor represents
+ /// </summary>
+ public sealed class LWStorageDescriptor : AsyncUpdatableResource, IObjectStorage, IEnumerable<KeyValuePair<string, string>>, IIndexable<string, string>
+ {
+
+ private static readonly JsonSerializerOptions SerializerOptions = new()
+ {
+ DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
+ NumberHandling = JsonNumberHandling.Strict,
+ ReadCommentHandling = JsonCommentHandling.Disallow,
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ IgnoreReadOnlyFields = true,
+ DefaultBufferSize = Environment.SystemPageSize,
+ };
+
+
+ internal LWStorageEntry Entry { get; }
+
+ private readonly Lazy<Dictionary<string, string>> StringStorage;
+
+ /// <summary>
+ /// The currnt descriptor's identifier string within its backing table. Usually the primary key.
+ /// </summary>
+ public string DescriptorID => Entry.Id;
+ /// <summary>
+ /// The identifier of the user for which this descriptor belongs to
+ /// </summary>
+ public string UserID => Entry.UserId!;
+ /// <summary>
+ /// The <see cref="DateTime"/> when the descriptor was created
+ /// </summary>
+ public DateTimeOffset Created => Entry.Created;
+ /// <summary>
+ /// The last time this descriptor was updated
+ /// </summary>
+ public DateTimeOffset LastModified => Entry.LastModified;
+
+ ///<inheritdoc/>
+ protected override AsyncUpdateCallback UpdateCb { get; }
+ ///<inheritdoc/>
+ protected override AsyncDeleteCallback DeleteCb { get; }
+ ///<inheritdoc/>
+ protected override JsonSerializerOptions JSO => SerializerOptions;
+
+ internal LWStorageDescriptor(LWStorageManager manager, LWStorageEntry entry)
+ {
+ Entry = entry;
+ UpdateCb = manager.UpdateDescriptorAsync;
+ DeleteCb = manager.RemoveDescriptorAsync;
+ StringStorage = new(OnStringStoreLoad);
+ }
+
+ internal Dictionary<string, string> OnStringStoreLoad()
+ {
+ if(Entry.Data == null || Entry.Data.Length == 0)
+ {
+ return new(StringComparer.OrdinalIgnoreCase);
+ }
+ else
+ {
+ //Calc and alloc decode buffer
+ int bufferSize = (int)(Entry.Data.Length * 1.75);
+ using UnsafeMemoryHandle<byte> decodeBuffer = Memory.UnsafeAlloc<byte>(bufferSize);
+
+ //Decode and deserialize the data
+ return BrotliDecoder.TryDecompress(Entry.Data, decodeBuffer, out int written)
+ ? JsonSerializer.Deserialize<Dictionary<string, string>>(Entry.Data, SerializerOptions) ?? new(StringComparer.OrdinalIgnoreCase)
+ : throw new InvalidDataException("Failed to decompress data");
+ }
+ }
+
+ /// <inheritdoc/>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public T? GetObject<T>(string key)
+ {
+ Check();
+ //De-serialize and return object
+ return StringStorage.Value.TryGetValue(key, out string? val) ? val.AsJsonObject<T>(SerializerOptions) : default;
+ }
+
+ /// <inheritdoc/>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public void SetObject<T>(string key, T obj)
+ {
+ //Remove the object from storage if its null
+ if (obj == null)
+ {
+ SetStringValue(key, null);
+ }
+ else
+ {
+ //Serialize the object to a string
+ string value = obj.ToJsonString(SerializerOptions)!;
+ //Attempt to store string in storage
+ SetStringValue(key, value);
+ }
+ }
+
+
+ /// <summary>
+ /// Gets a string value from string storage matching a given key
+ /// </summary>
+ /// <param name="key">Key for storage</param>
+ /// <returns>Value associaetd with key if exists, <see cref="string.Empty"/> otherwise</returns>
+ /// <exception cref="ArgumentNullException">If key is null</exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public string GetStringValue(string key)
+ {
+ Check();
+ return StringStorage.Value.TryGetValue(key, out string? val) ? val : string.Empty;
+ }
+
+ /// <summary>
+ /// Creates, overwrites, or removes a string value identified by key.
+ /// </summary>
+ /// <param name="key">Entry key</param>
+ /// <param name="value">String to store or overwrite, set to null or string.Empty to remove a property</param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentNullException">If key is null</exception>
+ public void SetStringValue(string key, string? value)
+ {
+ if (string.IsNullOrEmpty(key))
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+ Check();
+ //If the value is null, see if the the properties are null
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ //If the value is null and properies exist, remove the entry
+ StringStorage.Value.Remove(key);
+ Modified |= true;
+ }
+ else
+ {
+ //Set the value
+ StringStorage.Value[key] = value;
+ //Set modified flag
+ Modified |= true;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a string value from string storage matching a given key
+ /// </summary>
+ /// <param name="key">Key for storage</param>
+ /// <returns>Value associaetd with key if exists, <seealso cref="string.Empty "/> otherwise</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentNullException">If key is null</exception>
+ public string this[string key]
+ {
+ get => GetStringValue(key);
+ set => SetStringValue(key, value);
+ }
+
+ /// <summary>
+ /// Flushes all pending changes to the backing store asynchronously
+ /// </summary>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public ValueTask WritePendingChangesAsync()
+ {
+ Check();
+ return Modified ? (new(FlushPendingChangesAsync())) : ValueTask.CompletedTask;
+ }
+
+ ///<inheritdoc/>
+ public override async ValueTask ReleaseAsync()
+ {
+ await base.ReleaseAsync();
+ //Cleanup dict on exit
+ if (StringStorage.IsValueCreated)
+ {
+ StringStorage.Value.Clear();
+ }
+ }
+
+ ///<inheritdoc/>
+ public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => StringStorage.Value.GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ ///<inheritdoc/>
+ protected override object GetResource() => StringStorage.Value;
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageEntry.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageEntry.cs
new file mode 100644
index 0000000..5c5da61
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageEntry.cs
@@ -0,0 +1,44 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: LwStorageEntry.cs
+*
+* LwStorageEntry.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+using VNLib.Plugins.Extensions.Data.Abstractions;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+
+ internal sealed class LWStorageEntry : DbModelBase, IUserEntity
+ {
+ public override string Id { get; set; }
+
+ public override DateTime Created { get; set; }
+
+ public override DateTime LastModified { get; set; }
+
+ public string? UserId { get; set; }
+
+ public byte[]? Data { get; set; }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs
new file mode 100644
index 0000000..027fa90
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs
@@ -0,0 +1,347 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: LWStorageManager.cs
+*
+* LWStorageManager.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Data;
+using System.Linq;
+using System.Threading;
+using System.IO.Compression;
+using System.Threading.Tasks;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Utils;
+using VNLib.Utils.IO;
+using VNLib.Utils.Memory;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+
+ /// <summary>
+ /// Provides single table database object storage services
+ /// </summary>
+ public sealed class LWStorageManager
+ {
+ /// <summary>
+ /// The generator function that is invoked when a new <see cref="LWStorageDescriptor"/> is to
+ /// be created without an explicit id
+ /// </summary>
+ public Func<string> NewDescriptorIdGenerator { get; init; } = static () => Guid.NewGuid().ToString("N");
+
+ private readonly DbContextOptions DbOptions;
+ private readonly string TableName;
+
+ private LWStorageContext GetContext() => new(DbOptions, TableName);
+
+ /// <summary>
+ /// Creates a new <see cref="LWStorageManager"/> with
+ /// </summary>
+ /// <param name="options">The db context options to create database connections with</param>
+ /// <param name="tableName">The name of the table to operate on</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ public LWStorageManager(DbContextOptions options, string tableName)
+ {
+ DbOptions = options ?? throw new ArgumentNullException(nameof(options));
+ TableName = tableName ?? throw new ArgumentNullException(nameof(tableName));
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="LWStorageDescriptor"/> fror a given user
+ /// </summary>
+ /// <param name="userId">Id of user</param>
+ /// <param name="descriptorIdOverride">An override to specify the new descriptor's id</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>A new <see cref="LWStorageDescriptor"/> if successfully created, null otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="LWDescriptorCreationException"></exception>
+ public async Task<LWStorageDescriptor> CreateDescriptorAsync(string userId, string? descriptorIdOverride = null, CancellationToken cancellation = default)
+ {
+ if (string.IsNullOrWhiteSpace(userId))
+ {
+ throw new ArgumentNullException(nameof(userId));
+ }
+
+ //If no override id was specified, generate a new one
+ descriptorIdOverride ??= NewDescriptorIdGenerator();
+
+ DateTime createdOrModifedTime = DateTime.UtcNow;
+
+ await using LWStorageContext ctx = GetContext();
+ await ctx.OpenTransactionAsync(cancellation);
+
+ //Make sure the descriptor doesnt exist only by its descriptor id
+ if (await ctx.Descriptors.AnyAsync(d => d.Id == descriptorIdOverride, cancellation))
+ {
+ throw new LWDescriptorCreationException($"A descriptor with id {descriptorIdOverride} already exists");
+ }
+
+ //Cache time
+ DateTime now = DateTime.UtcNow;
+
+ //Create the new descriptor
+ LWStorageEntry entry = new()
+ {
+ Created = now,
+ LastModified = now,
+ Id = descriptorIdOverride,
+ UserId = userId,
+ };
+
+ //Add and save changes
+ ctx.Descriptors.Add(entry);
+
+ ERRNO result = await ctx.SaveChangesAsync(cancellation);
+
+ if (!result)
+ {
+ //Rollback and raise exception
+ await ctx.RollbackTransctionAsync(cancellation);
+ throw new LWDescriptorCreationException("Failed to create descriptor, because changes could not be saved");
+ }
+ else
+ {
+ //Commit transaction and return the new descriptor
+ await ctx.CommitTransactionAsync(cancellation);
+ return new LWStorageDescriptor(this, entry);
+ }
+ }
+
+ /// <summary>
+ /// Attempts to retrieve <see cref="LWStorageDescriptor"/> for a given user-id. The caller is responsible for
+ /// consitancy state of the descriptor
+ /// </summary>
+ /// <param name="userid">User's id</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>The descriptor belonging to the user, or null if not found or error occurs</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public async Task<LWStorageDescriptor?> GetDescriptorFromUIDAsync(string userid, CancellationToken cancellation = default)
+ {
+ //Allow null/empty entrys to just return null
+ if (string.IsNullOrWhiteSpace(userid))
+ {
+ throw new ArgumentNullException(nameof(userid));
+ }
+
+ //Init db
+ await using LWStorageContext db = GetContext();
+ //Begin transaction
+ await db.OpenTransactionAsync(cancellation);
+ //Get entry
+ LWStorageEntry? entry = await (from s in db.Descriptors
+ where s.UserId == userid
+ select s)
+ .SingleOrDefaultAsync(cancellation);
+
+ //Close transactions and return
+ if (entry == null)
+ {
+ await db.RollbackTransctionAsync(cancellation);
+ return null;
+ }
+ else
+ {
+ await db.CommitTransactionAsync(cancellation);
+ return new (this, entry);
+ }
+ }
+
+ /// <summary>
+ /// Attempts to retrieve the <see cref="LWStorageDescriptor"/> for the given descriptor id. The caller is responsible for
+ /// consitancy state of the descriptor
+ /// </summary>
+ /// <param name="descriptorId">Unique identifier for the descriptor</param>
+ /// <param name="cancellation">A token to cancel the opreeaiton</param>
+ /// <returns>The descriptor belonging to the user, or null if not found or error occurs</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public async Task<LWStorageDescriptor?> GetDescriptorFromIDAsync(string descriptorId, CancellationToken cancellation = default)
+ {
+ //Allow null/empty entrys to just return null
+ if (string.IsNullOrWhiteSpace(descriptorId))
+ {
+ throw new ArgumentNullException(nameof(descriptorId));
+ }
+
+ //Init db
+ await using LWStorageContext db = GetContext();
+ //Begin transaction
+ await db.OpenTransactionAsync(cancellation);
+ //Get entry
+ LWStorageEntry? entry = await (from s in db.Descriptors
+ where s.Id == descriptorId
+ select s)
+ .SingleOrDefaultAsync(cancellation);
+
+ //Close transactions and return
+ if (entry == null)
+ {
+ await db.RollbackTransctionAsync(cancellation);
+ return null;
+ }
+ else
+ {
+ await db.CommitTransactionAsync(cancellation);
+ return new (this, entry);
+ }
+ }
+
+ /// <summary>
+ /// Cleanup entries before the specified <see cref="TimeSpan"/>. Entires are store in UTC time
+ /// </summary>
+ /// <param name="compareTime">Time before <see cref="DateTime.UtcNow"/> to compare against</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>The number of entires cleaned</returns>S
+ public Task<ERRNO> CleanupTableAsync(TimeSpan compareTime, CancellationToken cancellation = default) => CleanupTableAsync(DateTime.UtcNow.Subtract(compareTime), cancellation);
+
+ /// <summary>
+ /// Cleanup entries before the specified <see cref="DateTime"/>. Entires are store in UTC time
+ /// </summary>
+ /// <param name="compareTime">UTC time to compare entires against</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>The number of entires cleaned</returns>
+ public async Task<ERRNO> CleanupTableAsync(DateTime compareTime, CancellationToken cancellation = default)
+ {
+ //Init db
+ await using LWStorageContext db = GetContext();
+ //Begin transaction
+ await db.OpenTransactionAsync(cancellation);
+
+ //Get all expired entires
+ LWStorageEntry[] expired = await (from s in db.Descriptors
+ where s.Created < compareTime
+ select s)
+ .ToArrayAsync(cancellation);
+
+ //Delete
+ db.Descriptors.RemoveRange(expired);
+
+ //Save changes
+ ERRNO count = await db.SaveChangesAsync(cancellation);
+
+ //Commit transaction
+ await db.CommitTransactionAsync(cancellation);
+
+ return count;
+ }
+
+ /// <summary>
+ /// Updates a descriptor's data field
+ /// </summary>
+ /// <param name="descriptorObj">Descriptor to update</param>
+ /// <param name="data">Data string to store to descriptor record</param>
+ /// <exception cref="LWStorageUpdateFailedException"></exception>
+ internal async Task UpdateDescriptorAsync(object descriptorObj, Stream data)
+ {
+ LWStorageEntry entry = (descriptorObj as LWStorageDescriptor)!.Entry;
+ ERRNO result = 0;
+ try
+ {
+ await using LWStorageContext ctx = GetContext();
+ await ctx.OpenTransactionAsync(CancellationToken.None);
+
+ //Begin tracking
+ ctx.Descriptors.Attach(entry);
+
+ //Convert stream to vnstream
+ VnMemoryStream vms = (VnMemoryStream)data;
+ using (IMemoryHandle<byte> encBuffer = Memory.SafeAlloc<byte>((int)vms.Length))
+ {
+ //try to compress
+ if(!BrotliEncoder.TryCompress(vms.AsSpan(), encBuffer.Span, out int compressed))
+ {
+ throw new InvalidDataException("Failed to compress the descriptor data");
+ }
+ //Set the data
+ entry.Data = encBuffer.Span.ToArray();
+ }
+ //Update modified time
+ entry.LastModified = DateTime.UtcNow;
+
+ //Save changes
+ result = await ctx.SaveChangesAsync(CancellationToken.None);
+
+ //Commit or rollback
+ if (result)
+ {
+ await ctx.CommitTransactionAsync(CancellationToken.None);
+ }
+ else
+ {
+ await ctx.RollbackTransctionAsync(CancellationToken.None);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new LWStorageUpdateFailedException("", ex);
+ }
+ //If the result is 0 then the update failed
+ if (!result)
+ {
+ throw new LWStorageUpdateFailedException($"Descriptor {entry.Id} failed to update");
+ }
+ }
+
+ /// <summary>
+ /// Function to remove the specified descriptor
+ /// </summary>
+ /// <param name="descriptorObj">The active descriptor to remove from the database</param>
+ /// <exception cref="LWStorageRemoveFailedException"></exception>
+ internal async Task RemoveDescriptorAsync(object descriptorObj)
+ {
+ LWStorageEntry descriptor = (descriptorObj as LWStorageDescriptor)!.Entry;
+ ERRNO result;
+ try
+ {
+ //Init db
+ await using LWStorageContext db = GetContext();
+ //Begin transaction
+ await db.OpenTransactionAsync();
+
+ //Delete the user from the database
+ db.Descriptors.Remove(descriptor);
+
+ //Save changes and commit if successful
+ result = await db.SaveChangesAsync();
+
+ if (result)
+ {
+ await db.CommitTransactionAsync();
+ }
+ else
+ {
+ await db.RollbackTransctionAsync();
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new LWStorageRemoveFailedException("", ex);
+ }
+ if (!result)
+ {
+ throw new LWStorageRemoveFailedException("Failed to delete the user account because of a database failure, the user may already be deleted");
+ }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageRemoveFailedException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageRemoveFailedException.cs
new file mode 100644
index 0000000..806912c
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageRemoveFailedException.cs
@@ -0,0 +1,44 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: LWStorageRemoveFailedException.cs
+*
+* LWStorageRemoveFailedException.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using VNLib.Utils.Resources;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+ /// <summary>
+ /// The exception raised when an open <see cref="LWStorageDescriptor"/> removal operation fails. The
+ /// <see cref="Exception.InnerException"/> property may contain any nested exceptions that caused the removal to fail.
+ /// </summary>
+ public class LWStorageRemoveFailedException : ResourceDeleteFailedException
+ {
+ internal LWStorageRemoveFailedException(string error, Exception inner) : base(error, inner) { }
+
+ public LWStorageRemoveFailedException()
+ {}
+
+ public LWStorageRemoveFailedException(string message) : base(message)
+ {}
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageUpdateFailedException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageUpdateFailedException.cs
new file mode 100644
index 0000000..fe555bf
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageUpdateFailedException.cs
@@ -0,0 +1,43 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: LWStorageUpdateFailedException.cs
+*
+* LWStorageUpdateFailedException.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using VNLib.Utils.Resources;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+ /// <summary>
+ /// The exception raised when an open <see cref="LWStorageDescriptor"/> update operation fails. The
+ /// <see cref="Exception.InnerException"/> property may contain any nested exceptions that caused the update to fail.
+ /// </summary>
+ public class LWStorageUpdateFailedException : ResourceUpdateFailedException
+ {
+ internal LWStorageUpdateFailedException(string error, Exception inner) : base(error, inner) { }
+
+ public LWStorageUpdateFailedException()
+ {}
+ public LWStorageUpdateFailedException(string message) : base(message)
+ {}
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/UndefinedBlobStateException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/UndefinedBlobStateException.cs
new file mode 100644
index 0000000..e845372
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/UndefinedBlobStateException.cs
@@ -0,0 +1,45 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: UndefinedBlobStateException.cs
+*
+* UndefinedBlobStateException.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Runtime.Serialization;
+
+namespace VNLib.Plugins.Extensions.Data.Storage
+{
+ /// <summary>
+ /// Raised to signal that the requested <see cref="Blob"/> was left in an undefined state
+ /// when previously accessed
+ /// </summary>
+ public class UndefinedBlobStateException : Exception
+ {
+ public UndefinedBlobStateException()
+ {}
+ public UndefinedBlobStateException(string message) : base(message)
+ {}
+ public UndefinedBlobStateException(string message, Exception innerException) : base(message, innerException)
+ {}
+ protected UndefinedBlobStateException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {}
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs
new file mode 100644
index 0000000..6b835c5
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs
@@ -0,0 +1,99 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: TransactionalDbContext.cs
+*
+* TransactionalDbContext.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Storage;
+
+namespace VNLib.Plugins.Extensions.Data
+{
+ public abstract class TransactionalDbContext : DbContext, IAsyncDisposable
+ {
+ /// <summary>
+ /// <inheritdoc/>
+ /// </summary>
+ protected TransactionalDbContext()
+ {}
+ /// <summary>
+ /// <inheritdoc/>
+ /// </summary>
+ protected TransactionalDbContext(DbContextOptions options) : base(options)
+ {}
+
+ /// <summary>
+ /// The transaction that was opened on the current context
+ /// </summary>
+ public IDbContextTransaction? Transaction { get; set; }
+
+
+#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize, ignore because base.Dispose() is called
+ ///<inheritdoc/>
+ public sealed override void Dispose()
+ {
+ //dispose the transaction
+ Transaction?.Dispose();
+ base.Dispose();
+ }
+
+ ///<inheritdoc/>
+ public override async ValueTask DisposeAsync()
+ {
+ //If transaction has been created, dispose the transaction
+ if (Transaction != null)
+ {
+ await Transaction.DisposeAsync();
+ }
+ await base.DisposeAsync();
+ }
+#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
+
+ /// <summary>
+ /// Opens a single transaction on the current context. If a transaction is already open,
+ /// it is disposed and a new transaction is begun.
+ /// </summary>
+ public async Task OpenTransactionAsync(CancellationToken cancellationToken = default)
+ {
+ //open a new transaction on the current database
+ this.Transaction = await base.Database.BeginTransactionAsync(cancellationToken);
+ }
+ /// <summary>
+ /// Invokes the <see cref="IDbContextTransaction.Commit"/> on the current context
+ /// </summary>
+ public Task CommitTransactionAsync(CancellationToken token = default)
+ {
+ return Transaction != null ? Transaction.CommitAsync(token) : Task.CompletedTask;
+ }
+ /// <summary>
+ /// Invokes the <see cref="IDbContextTransaction.Rollback"/> on the current context
+ /// </summary>
+ public Task RollbackTransctionAsync(CancellationToken token = default)
+ {
+ return Transaction != null ? Transaction.RollbackAsync(token) : Task.CompletedTask;
+ }
+
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj
new file mode 100644
index 0000000..a11f7c0
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj
@@ -0,0 +1,49 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <RootNamespace>VNLib.Plugins.Extensions.Data</RootNamespace>
+ <AssemblyName>VNLib.Plugins.Extensions.Data</AssemblyName>
+ <Authors>Vaughn Nugent</Authors>
+ <Description>Data extensions for VNLib Plugins</Description>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <Version>1.0.1.1</Version>
+ <Nullable>enable</Nullable>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ </PropertyGroup>
+
+ <!-- Resolve nuget dll files and store them in the output dir -->
+ <PropertyGroup>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.11" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.11" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\VNLib\Utils\src\VNLib.Utils.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.xml b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.xml
new file mode 100644
index 0000000..2f7736e
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.xml
@@ -0,0 +1,476 @@
+<?xml version="1.0"?>
+<!--
+Copyright (c) 2022 Vaughn Nugent
+-->
+<doc>
+ <assembly>
+ <name>VNLib.Plugins.Extensions.Data</name>
+ </assembly>
+ <members>
+ <member name="T:VNLib.Plugins.Extensions.Data.Abstractions.IBulkDataStore`1">
+ <summary>
+ An abstraction that defines a Data-Store that supports
+ bulk data operations
+ </summary>
+ <typeparam name="T">The data-model type</typeparam>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IBulkDataStore`1.DeleteBulkAsync(System.Collections.Generic.ICollection{`0})">
+ <summary>
+ Deletes a collection of records from the store
+ </summary>
+ <param name="records">A collection of records to delete</param>
+ <returns>A task the resolves the number of entires removed from the store</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IBulkDataStore`1.UpdateBulkAsync(System.Collections.Generic.ICollection{`0})">
+ <summary>
+ Updates a collection of records
+ </summary>
+ <param name="records">The collection of records to update</param>
+ <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IBulkDataStore`1.CreateBulkAsync(System.Collections.Generic.ICollection{`0})">
+ <summary>
+ Creates a bulk collection of records as entries in the store
+ </summary>
+ <param name="records">The collection of records to add</param>
+ <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IBulkDataStore`1.AddOrUpdateBulkAsync(System.Collections.Generic.ICollection{`0})">
+ <summary>
+ Creates or updates individual records from a bulk collection of records
+ </summary>
+ <param name="records">The collection of records to add</param>
+ <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1">
+ <summary>
+ An abstraction that defines a Data-Store and common
+ operations that retrieve or manipulate records of data
+ </summary>
+ <typeparam name="T">The data-model type</typeparam>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetCountAsync">
+ <summary>
+ Gets the total number of records in the current store
+ </summary>
+ <returns>A task that resolves the number of records in the store</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetCountAsync(System.String)">
+ <summary>
+ Gets the number of records that belong to the specified constraint
+ </summary>
+ <param name="specifier">A specifier to constrain the reults</param>
+ <returns>The number of records that belong to the specifier</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetSingleAsync(System.String)">
+ <summary>
+ Gets a record from its key
+ </summary>
+ <param name="key">The key identifying the unique record</param>
+ <returns>A promise that resolves the record identified by the specified key</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetSingleAsync(System.String[])">
+ <summary>
+ Gets a record from its key
+ </summary>
+ <param name="specifiers">A variable length specifier arguemnt array for retreiving a single application</param>
+ <returns></returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetSingleAsync(`0)">
+ <summary>
+ Gets a record from the store with a partial model, intended to complete the model
+ </summary>
+ <param name="record">The partial model used to query the store</param>
+ <returns>A task the resolves the completed data-model</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetCollectionAsync(System.Collections.Generic.ICollection{`0},System.String,System.Int32)">
+ <summary>
+ Fills a collection with enires retireved from the store using the specifer
+ </summary>
+ <param name="collection">The collection to add entires to</param>
+ <param name="specifier">A specifier argument to constrain results</param>
+ <param name="limit">The maximum number of elements to retrieve</param>
+ <returns>A Task the resolves to the number of items added to the collection</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.GetCollectionAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.String[])">
+ <summary>
+ Fills a collection with enires retireved from the store using a variable length specifier
+ parameter
+ </summary>
+ <param name="collection">The collection to add entires to</param>
+ <param name="limit">The maximum number of elements to retrieve</param>
+ <param name="args"></param>
+ <returns>A Task the resolves to the number of items added to the collection</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.UpdateAsync(`0)">
+ <summary>
+ Updates an entry in the store with the specified record
+ </summary>
+ <param name="record">The record to update</param>
+ <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.CreateAsync(`0)">
+ <summary>
+ Creates a new entry in the store representing the specified record
+ </summary>
+ <param name="record">The record to add to the store</param>
+ <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.DeleteAsync(`0)">
+ <summary>
+ Deletes one or more entrires from the store matching the specified record
+ </summary>
+ <param name="record">The record to remove from the store</param>
+ <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.DeleteAsync(System.String)">
+ <summary>
+ Deletes one or more entires from the store matching the specified unique key
+ </summary>
+ <param name="key">The unique key that identifies the record</param>
+ <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.DeleteAsync(System.String[])">
+ <summary>
+ Deletes one or more entires from the store matching the supplied specifiers
+ </summary>
+ <param name="specifiers">A variable length array of specifiers used to delete one or more entires</param>
+ <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IDataStore`1.AddOrUpdateAsync(`0)">
+ <summary>
+ Updates an entry in the store if it exists, or creates a new entry if one does not already exist
+ </summary>
+ <param name="record">The record to add to the store</param>
+ <returns>A task the resolves the result of the operation</returns>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Data.Abstractions.IPaginatedDataStore`1">
+ <summary>
+ Defines a Data-Store that can retirieve and manipulate paginated
+ data
+ </summary>
+ <typeparam name="T">The data-model type</typeparam>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IPaginatedDataStore`1.GetPageAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.Int32)">
+ <summary>
+ Gets a collection of records using a pagination style query, and adds the records to the collecion
+ </summary>
+ <param name="collection">The collection to add records to</param>
+ <param name="page">Pagination page to get records from</param>
+ <param name="limit">The maximum number of items to retrieve from the store</param>
+ <returns>A task that resolves the number of items added to the collection</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.Abstractions.IPaginatedDataStore`1.GetPageAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.Int32,System.String[])">
+ <summary>
+ Gets a collection of records using a pagination style query with constraint arguments, and adds the records to the collecion
+ </summary>
+ <param name="collection">The collection to add records to</param>
+ <param name="page">Pagination page to get records from</param>
+ <param name="limit">The maximum number of items to retrieve from the store</param>
+ <param name="constraints">A params array of strings to constrain the result set from the db</param>
+ <returns>A task that resolves the number of items added to the collection</returns>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Data.Abstractions.IUserEntity">
+ <summary>
+ Defines an entity base that has an owner, identified by its user-id
+ </summary>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.Abstractions.IUserEntity.UserId">
+ <summary>
+ The user-id of the owner of the entity
+ </summary>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Data.DbModelBase">
+ <summary>
+ Provides a base for DBSet Records with a timestamp/version
+ a unique ID key, and create/modified timestamps
+ </summary>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.DbModelBase.Id">
+ <inheritdoc/>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.DbModelBase.Version">
+ <inheritdoc/>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.DbModelBase.Created">
+ <inheritdoc/>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.DbModelBase.LastModified">
+ <inheritdoc/>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Data.DbStore`1">
+ <summary>
+ Implements basic data-store functionality with abstract query builders
+ </summary>
+ <typeparam name="T">A <see cref="T:VNLib.Plugins.Extensions.Data.DbModelBase"/> implemented type</typeparam>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.DbStore`1.RecordIdBuilder">
+ <summary>
+ Gets a unique ID for a new record being added to the store
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.NewContext">
+ <summary>
+ Gets a new <see cref="T:VNLib.Plugins.Extensions.Data.TransactionalDbContext"/> ready for use
+ </summary>
+ <returns></returns>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.DbStore`1.ListRental">
+ <summary>
+ An object rental for entity collections
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.AddOrUpdateAsync(`0)">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.UpdateAsync(`0)">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.CreateAsync(`0)">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.AddOrUpdateQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,`0)">
+ <summary>
+ Builds a query that attempts to get a single entry from the
+ store based on the specified record if it does not have a
+ valid <see cref="P:VNLib.Plugins.Extensions.Data.DbModelBase.Id"/> property
+ </summary>
+ <param name="context">The active context to query</param>
+ <param name="record">The record to search for</param>
+ <returns>A query that yields a single record if it exists in the store</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.UpdateQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,`0)">
+ <summary>
+ Builds a query that attempts to get a single entry from the
+ store to update based on the specified record
+ </summary>
+ <param name="context">The active context to query</param>
+ <param name="record">The record to search for</param>
+ <returns>A query that yields a single record to update if it exists in the store</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.OnRecordUpdate(`0,`0)">
+ <summary>
+ Updates the current record (if found) to the new record before
+ storing the updates.
+ </summary>
+ <param name="newRecord">The new record to capture data from</param>
+ <param name="currentRecord">The current record to be updated</param>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.DeleteAsync(System.String)">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.DeleteAsync(`0)">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.DeleteAsync(System.String[])">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.DeleteQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])">
+ <summary>
+ Builds a query that results in a single entry to delete from the
+ constraint arguments
+ </summary>
+ <param name="context">The active context</param>
+ <param name="constraints">A variable length parameter array of query constraints</param>
+ <returns>A query that yields a single record (or no record) to delete</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCollectionAsync(System.Collections.Generic.ICollection{`0},System.String,System.Int32)">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCollectionAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.String[])">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCollectionQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String)">
+ <summary>
+ Builds a query to get a count of records constrained by the specifier
+ </summary>
+ <param name="context">The active context to run the query on</param>
+ <param name="specifier">The specifier constrain</param>
+ <returns>A query that can be counted</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCollectionQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])">
+ <summary>
+ Builds a query to get a collection of records based on an variable length array of parameters
+ </summary>
+ <param name="context">The active context to run the query on</param>
+ <param name="constraints">An arguments array to constrain the results of the query</param>
+ <returns>A query that returns a collection of records from the store</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCountAsync">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCountAsync(System.String)">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetCountQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String)">
+ <summary>
+ Builds a query to get a count of records constrained by the specifier
+ </summary>
+ <param name="context">The active context to run the query on</param>
+ <param name="specifier">The specifier constrain</param>
+ <returns>A query that can be counted</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetSingleAsync(System.String)">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetSingleAsync(`0)">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetSingleAsync(System.String[])">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetSingleQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])">
+ <summary>
+ Builds a query to get a single record from the variable length parameter arguments
+ </summary>
+ <param name="context">The context to execute query against</param>
+ <param name="constraints">Arguments to constrain the results of the query to a single record</param>
+ <returns>A query that yields a single record</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetSingleQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,`0)">
+ <summary>
+ <para>
+ Builds a query to get a single record from the specified record.
+ </para>
+ <para>
+ Unless overridden, performs an ID based query for a single entry
+ </para>
+ </summary>
+ <param name="context">The context to execute query against</param>
+ <param name="record">A record to referrence the lookup</param>
+ <returns>A query that yields a single record</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetPageAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.Int32)">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetPageAsync(System.Collections.Generic.ICollection{`0},System.Int32,System.Int32,System.String[])">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.DbStore`1.GetPageQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])">
+ <summary>
+ Builds a query to get a collection of records based on an variable length array of parameters
+ </summary>
+ <param name="context">The active context to run the query on</param>
+ <param name="constraints">An arguments array to constrain the results of the query</param>
+ <returns>A query that returns a paginated collection of records from the store</returns>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Data.IDbModel">
+ <summary>
+ Represents a basic data model for an EFCore entity
+ for support in data-stores
+ </summary>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.IDbModel.Id">
+ <summary>
+ A unique id for the entity
+ </summary>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.IDbModel.Created">
+ <summary>
+ The <see cref="T:System.DateTimeOffset"/> the entity was created in the store
+ </summary>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.IDbModel.LastModified">
+ <summary>
+ The <see cref="T:System.DateTimeOffset"/> the entity was last modified in the store
+ </summary>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.IDbModel.Version">
+ <summary>
+ Entity concurrency token
+ </summary>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Data.ProtectedDbStore`1">
+ <summary>
+ A data store that provides unique identities and protections based on an entity that has an owner <see cref="T:VNLib.Plugins.Extensions.Data.Abstractions.IUserEntity"/>
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.ProtectedDbStore`1.GetCollectionQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.ProtectedDbStore`1.GetSingleQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,System.String[])">
+ <summary>
+ Gets a single item contrained by a given user-id and item id
+ </summary>
+ <param name="context"></param>
+ <param name="constraints"></param>
+ <returns></returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.ProtectedDbStore`1.GetSingleQueryBuilder(VNLib.Plugins.Extensions.Data.TransactionalDbContext,`0)">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.ProtectedEntityExtensions.UpdateAsync``1(VNLib.Plugins.Extensions.Data.Abstractions.IDataStore{``0},``0,System.String)">
+ <summary>
+ Updates the specified record within the store
+ </summary>
+ <param name="store"></param>
+ <param name="record">The record to update</param>
+ <param name="userId">The userid of the record owner</param>
+ <returns>A task that evaluates to the number of records modified</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.ProtectedEntityExtensions.CreateAsync``1(VNLib.Plugins.Extensions.Data.Abstractions.IDataStore{``0},``0,System.String)">
+ <summary>
+ Updates the specified record within the store
+ </summary>
+ <param name="store"></param>
+ <param name="record">The record to update</param>
+ <param name="userId">The userid of the record owner</param>
+ <returns>A task that evaluates to the number of records modified</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.ProtectedEntityExtensions.GetSingleAsync``1(VNLib.Plugins.Extensions.Data.Abstractions.IDataStore{``0},System.String,System.String)">
+ <summary>
+ Gets a single entity from its ID and user-id
+ </summary>
+ <param name="store"></param>
+ <param name="key">The unique id of the entity</param>
+ <param name="userId">The user's id that owns the resource</param>
+ <returns>A task that resolves the entity or null if not found</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.ProtectedEntityExtensions.DeleteAsync``1(VNLib.Plugins.Extensions.Data.Abstractions.IDataStore{``0},System.String,System.String)">
+ <summary>
+ Deletes a single entiry by its ID only if it belongs to the speicifed user
+ </summary>
+ <param name="store"></param>
+ <param name="key">The unique id of the entity</param>
+ <param name="userId">The user's id that owns the resource</param>
+ <returns>A task the resolves the number of eneities deleted (should evaluate to true or false)</returns>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.#ctor">
+ <summary>
+ <inheritdoc/>
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.#ctor(Microsoft.EntityFrameworkCore.DbContextOptions)">
+ <summary>
+ <inheritdoc/>
+ </summary>
+ </member>
+ <member name="P:VNLib.Plugins.Extensions.Data.TransactionalDbContext.Transaction">
+ <summary>
+ The transaction that was opened on the current context
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.Dispose">
+ <inheritdoc/>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.OpenTransactionAsync(System.Threading.CancellationToken)">
+ <summary>
+ Opens a single transaction on the current context. If a transaction is already open,
+ it is disposed and a new transaction is begun.
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.CommitTransactionAsync(System.Threading.CancellationToken)">
+ <summary>
+ Invokes the <see cref="M:Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction.Commit"/> on the current context
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.RollbackTransctionAsync(System.Threading.CancellationToken)">
+ <summary>
+ Invokes the <see cref="M:Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction.Rollback"/> on the current context
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Data.TransactionalDbContext.DisposeAsync">
+ <inheritdoc/>
+ </member>
+ </members>
+</doc>
diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/LICENSE.txt b/lib/VNLib.Plugins.Extensions.Loading.Sql/LICENSE.txt
new file mode 100644
index 0000000..147bcd6
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/LICENSE.txt
@@ -0,0 +1,195 @@
+Copyright (c) 2022 Vaughn Nugent
+
+Contact information
+ Name: Vaughn Nugent
+ Email: public[at]vaughnnugent[dot]com
+ Website: https://www.vaughnnugent.com
+
+The software in this repository is licensed under the GNU Affero GPL version 3.0 (or any later version).
+
+GNU AFFERO GENERAL PUBLIC LICENSE
+
+Version 3, 19 November 2007
+
+Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+Preamble
+
+The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
+
+The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
+
+An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
+
+The precise terms and conditions for copying, distribution and modification follow.
+TERMS AND CONDITIONS
+0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the Program.
+
+To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
+1. Source Code.
+
+The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+2. Basic Permissions.
+
+All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
+4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
+5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
+ b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
+ c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
+ d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
+6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
+ b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
+ c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
+ d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
+ e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
+7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
+ b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
+ c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
+ d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
+ e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
+ f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
+12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
+13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
+
+Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/README.md b/lib/VNLib.Plugins.Extensions.Loading.Sql/README.md
new file mode 100644
index 0000000..52309e1
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/README.md
@@ -0,0 +1,5 @@
+# VNLib.Plugins.Extensions.Loading.Sql
+*An extension library for mindless sql connection building from a plugin's configuration*
+
+#### Builds
+Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my [website](https://www.vaughnnugent.com/resources/software). All tar-gzip (.tgz) files will have an associated .sha384 appended checksum of the desired download file. \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs
new file mode 100644
index 0000000..c230cf5
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs
@@ -0,0 +1,174 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading.Sql
+* File: SqlDbConnectionLoader.cs
+*
+* SqlDbConnectionLoader.cs is part of VNLib.Plugins.Extensions.Loading.Sql which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading.Sql is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading.Sql is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading.Sql. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Text.Json;
+using System.Data.Common;
+
+using MySqlConnector;
+
+using Microsoft.Data.Sqlite;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Plugins.Extensions.Loading.Sql
+{
+ /// <summary>
+ /// Provides common basic SQL loading extensions for plugins
+ /// </summary>
+ public static class SqlDbConnectionLoader
+ {
+ public const string SQL_CONFIG_KEY = "sql";
+ public const string DB_PASSWORD_KEY = "db_password";
+
+
+ /// <summary>
+ /// Gets (or loads) the ambient sql connection factory for the current plugin
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The ambient <see cref="DbConnection"/> factory</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static Func<DbConnection> GetConnectionFactory(this PluginBase plugin)
+ {
+ plugin.ThrowIfUnloaded();
+ //Get or load
+ return LoadingExtensions.GetOrCreateSingleton(plugin, FactoryLoader);
+ }
+
+ private static Func<DbConnection> FactoryLoader(PluginBase plugin)
+ {
+ IReadOnlyDictionary<string, JsonElement> sqlConf = plugin.GetConfig(SQL_CONFIG_KEY);
+
+ //Get the db-type
+ string? type = sqlConf.GetPropString("db_type");
+
+ if ("sqlite".Equals(type, StringComparison.OrdinalIgnoreCase))
+ {
+ //Use connection builder
+ DbConnectionStringBuilder sqlBuilder = new SqliteConnectionStringBuilder()
+ {
+ DataSource = sqlConf["source"].GetString(),
+ };
+ string connectionString = sqlBuilder.ToString();
+ DbConnection DbFactory() => new SqliteConnection(connectionString);
+ return DbFactory;
+ }
+ else if("mysql".Equals(type, StringComparison.OrdinalIgnoreCase))
+ {
+ using SecretResult? password = plugin.TryGetSecretAsync(DB_PASSWORD_KEY).Result;
+
+ DbConnectionStringBuilder sqlBuilder = new MySqlConnectionStringBuilder()
+ {
+ Server = sqlConf["hostname"].GetString(),
+ Database = sqlConf["database"].GetString(),
+ UserID = sqlConf["username"].GetString(),
+ Password = password?.Result.ToString(),
+ Pooling = true,
+ LoadBalance = MySqlLoadBalance.LeastConnections,
+ MinimumPoolSize = sqlConf["min_pool_size"].GetUInt32()
+ };
+
+ string connectionString = sqlBuilder.ToString();
+ DbConnection DbFactory() => new MySqlConnection(connectionString);
+ return DbFactory;
+ }
+ //Default to mssql
+ else
+ {
+ using SecretResult? password = plugin.TryGetSecretAsync(DB_PASSWORD_KEY).Result;
+
+ //Use connection builder
+ DbConnectionStringBuilder sqlBuilder = new SqlConnectionStringBuilder()
+ {
+ DataSource = sqlConf["hostname"].GetString(),
+ UserID = sqlConf["username"].GetString(),
+ Password = password?.Result.ToString(),
+ InitialCatalog = sqlConf["catalog"].GetString(),
+ IntegratedSecurity = sqlConf["ms_security"].GetBoolean(),
+ Pooling = true,
+ MinPoolSize = sqlConf["min_pool_size"].GetInt32(),
+ Replication = true
+ };
+ string connectionString = sqlBuilder.ToString();
+ DbConnection DbFactory() => new SqlConnection(connectionString);
+ return DbFactory;
+ }
+ }
+
+ /// <summary>
+ /// Gets (or loads) the ambient <see cref="DbContextOptions"/> configured from
+ /// the ambient sql factory
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The ambient <see cref="DbContextOptions"/> for the current plugin</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <remarks>If plugin is in debug mode, writes log data to the default log</remarks>
+ public static DbContextOptions GetContextOptions(this PluginBase plugin)
+ {
+ plugin.ThrowIfUnloaded();
+ return LoadingExtensions.GetOrCreateSingleton(plugin, GetDbOptionsLoader);
+ }
+
+ private static DbContextOptions GetDbOptionsLoader(PluginBase plugin)
+ {
+ //Get a db connection object
+ using DbConnection connection = plugin.GetConnectionFactory().Invoke();
+ DbContextOptionsBuilder builder = new();
+
+ //Determine connection type
+ if(connection is SqlConnection sql)
+ {
+ //Use sql server from connection
+ builder.UseSqlServer(sql.ConnectionString);
+ }
+ else if(connection is SqliteConnection slc)
+ {
+ builder.UseSqlite(slc.ConnectionString);
+ }
+ else if(connection is MySqlConnection msconn)
+ {
+ //Detect version
+ ServerVersion version = ServerVersion.AutoDetect(msconn);
+
+ builder.UseMySql(msconn.ConnectionString, version);
+ }
+
+ //Enable logging
+ if(plugin.IsDebug())
+ {
+ builder.LogTo(plugin.Log.Debug);
+ }
+
+ //Get context and freez it before returning
+ DbContextOptions options = builder.Options;
+ options.Freeze();
+ return options;
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj
new file mode 100644
index 0000000..ea876c1
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj
@@ -0,0 +1,41 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <Version>1.0.1.1</Version>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="6.0.11" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.11" />
+ <PackageReference Include="MySqlConnector" Version="2.2.0" />
+ <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\VNLib\Utils\src\VNLib.Utils.csproj" />
+ <ProjectReference Include="..\..\PluginBase\VNLib.Plugins.PluginBase.csproj" />
+ <ProjectReference Include="..\VNLib.Plugins.Extensions.Loading\VNLib.Plugins.Extensions.Loading.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/lib/VNLib.Plugins.Extensions.Loading/LICENSE.txt b/lib/VNLib.Plugins.Extensions.Loading/LICENSE.txt
new file mode 100644
index 0000000..147bcd6
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/LICENSE.txt
@@ -0,0 +1,195 @@
+Copyright (c) 2022 Vaughn Nugent
+
+Contact information
+ Name: Vaughn Nugent
+ Email: public[at]vaughnnugent[dot]com
+ Website: https://www.vaughnnugent.com
+
+The software in this repository is licensed under the GNU Affero GPL version 3.0 (or any later version).
+
+GNU AFFERO GENERAL PUBLIC LICENSE
+
+Version 3, 19 November 2007
+
+Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+Preamble
+
+The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
+
+The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
+
+An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
+
+The precise terms and conditions for copying, distribution and modification follow.
+TERMS AND CONDITIONS
+0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the Program.
+
+To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
+1. Source Code.
+
+The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+2. Basic Permissions.
+
+All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
+4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
+5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
+ b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
+ c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
+ d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
+6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
+ b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
+ c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
+ d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
+ e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
+7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
+ b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
+ c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
+ d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
+ e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
+ f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
+12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
+13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
+
+Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Loading/README.md b/lib/VNLib.Plugins.Extensions.Loading/README.md
new file mode 100644
index 0000000..9e82a7c
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/README.md
@@ -0,0 +1,5 @@
+# VNLib.Plugins.Extensions.Loading
+*An extension library for reducing plugin initialization complexity with standarized configuration, secret loading, rapid endpoint initialization, event handling and more*
+
+#### Builds
+Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my [website](https://www.vaughnnugent.com/resources/software). All tar-gzip (.tgz) files will have an associated .sha384 appended checksum of the desired download file. \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs
new file mode 100644
index 0000000..5baf123
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs
@@ -0,0 +1,162 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: AssemblyLoader.cs
+*
+* AssemblyLoader.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Collections.Generic;
+
+using McMaster.NETCore.Plugins;
+
+using VNLib.Utils.Resources;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// <para>
+ /// Represents a disposable assembly loader wrapper for
+ /// exporting a signle type from a loaded assembly
+ /// </para>
+ /// <para>
+ /// If the loaded type implements <see cref="IDisposable"/> the
+ /// dispose method is called when the loader is disposed
+ /// </para>
+ /// </summary>
+ /// <typeparam name="T">The exported type to manage</typeparam>
+ public class AssemblyLoader<T> : OpenResourceHandle<T>
+ {
+ private readonly PluginLoader _loader;
+ private readonly CancellationTokenRegistration _reg;
+ private readonly Lazy<T> _instance;
+
+ /// <summary>
+ /// The instance of the loaded type
+ /// </summary>
+ public override T Resource => _instance.Value;
+
+ private AssemblyLoader(PluginLoader loader, in CancellationToken unloadToken)
+ {
+ _loader = loader;
+ //Init lazy loader
+ _instance = new(LoadAndGetExportedType, LazyThreadSafetyMode.PublicationOnly);
+ //Register dispose
+ _reg = unloadToken.Register(Dispose);
+ }
+
+ /// <summary>
+ /// Loads the default assembly and gets the expected export type,
+ /// creates a new instance, and calls its parameterless constructor
+ /// </summary>
+ /// <returns>The desired type instance</returns>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ private T LoadAndGetExportedType()
+ {
+ //Load the assembly
+ Assembly asm = _loader.LoadDefaultAssembly();
+
+ Type resourceType = typeof(T);
+
+ //See if the type is exported
+ Type exp = (from type in asm.GetExportedTypes()
+ where resourceType.IsAssignableFrom(type)
+ select type)
+ .FirstOrDefault()
+ ?? throw new EntryPointNotFoundException($"Imported assembly does not export desired type {resourceType.FullName}");
+ //Create instance
+ return (T)Activator.CreateInstance(exp)!;
+ }
+
+ /// <summary>
+ /// Creates a method delegate for the given method name from
+ /// the instance wrapped by the current loader
+ /// </summary>
+ /// <typeparam name="TDelegate"></typeparam>
+ /// <param name="methodName">The name of the method to recover</param>
+ /// <returns>The delegate method wrapper if found, null otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="AmbiguousMatchException"></exception>
+ public TDelegate? TryGetMethod<TDelegate>(string methodName) where TDelegate : Delegate
+ {
+ //get the type info of the actual resource
+ return Resource!.GetType()
+ .GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)
+ ?.CreateDelegate<TDelegate>(Resource);
+ }
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ //If the instance is disposable, call its dispose method on unload
+ if (_instance.IsValueCreated && _instance.Value is IDisposable)
+ {
+ (_instance.Value as IDisposable)?.Dispose();
+ }
+ _loader.Dispose();
+ _reg.Dispose();
+ }
+
+ /// <summary>
+ /// Creates a new assembly loader for the specified type and
+ /// </summary>
+ /// <param name="assemblyName">The name of the assmbly within the current plugin directory</param>
+ /// <param name="unloadToken">The plugin unload token</param>
+ internal static AssemblyLoader<T> Load(string assemblyName, CancellationToken unloadToken)
+ {
+ Assembly executingAsm = Assembly.GetExecutingAssembly();
+ AssemblyLoadContext currentCtx = AssemblyLoadContext.GetLoadContext(executingAsm) ?? throw new InvalidOperationException("Could not get default assembly load context");
+
+ List<Type> shared = new ()
+ {
+ typeof(T),
+ typeof(PluginBase),
+ };
+
+ //Share all VNLib internal libraries
+ shared.AddRange(currentCtx.Assemblies.Where(static s => s.FullName.Contains("VNLib", StringComparison.OrdinalIgnoreCase)).SelectMany(static s => s.GetExportedTypes()));
+
+ PluginLoader loader = PluginLoader.CreateFromAssemblyFile(assemblyName,
+ currentCtx.IsCollectible,
+ shared.ToArray(),
+ conf =>
+ {
+
+ /*
+ * Load context is required to be set to the executing assembly's load context
+ * because it is controlled by the host, so this loader should be considered a
+ * a "child" collection of assemblies
+ */
+ conf.DefaultContext = currentCtx;
+
+ conf.PreferSharedTypes = true;
+
+ //Share utils asm
+ conf.SharedAssemblies.Add(typeof(Utils.Memory.Memory).Assembly.GetName());
+ });
+
+ return new(loader, in unloadToken);
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
new file mode 100644
index 0000000..18df8e0
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
@@ -0,0 +1,199 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: ConfigurationExtensions.cs
+*
+* ConfigurationExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Text.Json;
+using System.Reflection;
+using System.Collections.Generic;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// Specifies a configuration variable name in the plugin's configuration
+ /// containing data specific to the type
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class)]
+ public sealed class ConfigurationNameAttribute : Attribute
+ {
+ /// <summary>
+ ///
+ /// </summary>
+ public string ConfigVarName { get; }
+
+ /// <summary>
+ /// Initializes a new <see cref="ConfigurationNameAttribute"/>
+ /// </summary>
+ /// <param name="configVarName">The name of the configuration variable for the class</param>
+ public ConfigurationNameAttribute(string configVarName)
+ {
+ ConfigVarName = configVarName;
+ }
+ }
+
+ /// <summary>
+ /// Contains extensions for plugin configuration specifc extensions
+ /// </summary>
+ public static class ConfigurationExtensions
+ {
+ public const string S3_CONFIG = "s3_config";
+ public const string S3_SECRET_KEY = "s3_secret";
+
+ /// <summary>
+ /// Retrieves a top level configuration dictionary of elements for the specified type.
+ /// The type must contain a <see cref="ConfigurationNameAttribute"/>
+ /// </summary>
+ /// <typeparam name="T">The type to get the configuration of</typeparam>
+ /// <param name="plugin"></param>
+ /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IReadOnlyDictionary<string, JsonElement> GetConfigForType<T>(this PluginBase plugin)
+ {
+ Type t = typeof(T);
+ return plugin.GetConfigForType(t);
+ }
+ /// <summary>
+ /// Retrieves a top level configuration dictionary of elements with the specified property name,
+ /// from the plugin config first, or falls back to the host config file
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="propName">The config property name to retrieve</param>
+ /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IReadOnlyDictionary<string, JsonElement> GetConfig(this PluginBase plugin, string propName)
+ {
+ plugin.ThrowIfUnloaded();
+ try
+ {
+ //Try to get the element from the plugin config first
+ if (!plugin.PluginConfig.TryGetProperty(propName, out JsonElement el))
+ {
+ //Fallback to the host config
+ el = plugin.HostConfig.GetProperty(propName);
+ }
+ //Get the top level config as a dictionary
+ return el.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value);
+ }
+ catch(KeyNotFoundException)
+ {
+ throw new KeyNotFoundException($"Missing required top level configuration object '{propName}', in host/plugin configuration files");
+ }
+ }
+ /// <summary>
+ /// Retrieves a top level configuration dictionary of elements with the specified property name,
+ /// from the plugin config first, or falls back to the host config file
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="propName">The config property name to retrieve</param>
+ /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IReadOnlyDictionary<string, JsonElement>? TryGetConfig(this PluginBase plugin, string propName)
+ {
+ plugin.ThrowIfUnloaded();
+ //Try to get the element from the plugin config first, or fallback to host
+ if (plugin.PluginConfig.TryGetProperty(propName, out JsonElement el) || plugin.HostConfig.TryGetProperty(propName, out el))
+ {
+ //Get the top level config as a dictionary
+ return el.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value);
+ }
+ //No config found
+ return null;
+ }
+
+ /// <summary>
+ /// Retrieves a top level configuration dictionary of elements for the specified type.
+ /// The type must contain a <see cref="ConfigurationNameAttribute"/>
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="type">The type to get configuration data for</param>
+ /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IReadOnlyDictionary<string, JsonElement> GetConfigForType(this PluginBase plugin, Type type)
+ {
+ //Get config name attribute from plugin type
+ ConfigurationNameAttribute? configName = type.GetCustomAttribute<ConfigurationNameAttribute>();
+ return configName?.ConfigVarName == null
+ ? throw new KeyNotFoundException("No configuration attribute set")
+ : plugin.GetConfig(configName.ConfigVarName);
+ }
+
+ /// <summary>
+ /// Shortcut extension for <see cref="GetConfigForType{T}(PluginBase)"/> to get
+ /// config of current class
+ /// </summary>
+ /// <param name="obj">The object that a configuration can be retrieved for</param>
+ /// <param name="plugin">The plugin containing configuration variables</param>
+ /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IReadOnlyDictionary<string, JsonElement> GetConfig(this PluginBase plugin, object obj)
+ {
+ Type t = obj.GetType();
+ return plugin.GetConfigForType(t);
+ }
+
+ /// <summary>
+ /// Determines if the current plugin configuration contains the require properties to initialize
+ /// the type
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="plugin"></param>
+ /// <returns>True if the plugin config contains the require configuration property</returns>
+ public static bool HasConfigForType<T>(this PluginBase plugin)
+ {
+ Type type = typeof(T);
+ ConfigurationNameAttribute? configName = type.GetCustomAttribute<ConfigurationNameAttribute>();
+ //See if the plugin contains a configuration varables
+ return configName != null && plugin.PluginConfig.TryGetProperty(configName.ConfigVarName, out _);
+ }
+
+ /// <summary>
+ /// Attempts to load the basic S3 configuration variables required
+ /// for S3 client access
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The S3 configuration object found in the plugin/host configuration</returns>
+ public static S3Config? TryGetS3Config(this PluginBase plugin)
+ {
+ //Try get the config
+ IReadOnlyDictionary<string, JsonElement>? s3conf = plugin.TryGetConfig(S3_CONFIG);
+ if(s3conf == null)
+ {
+ return null;
+ }
+
+ //Try get the elements
+ return new()
+ {
+ BaseBucket = s3conf.GetPropString("bucket"),
+ ClientId = s3conf.GetPropString("access_key"),
+ ServerAddress = s3conf.GetPropString("server_address"),
+ UseSsl = s3conf.TryGetValue("use_ssl", out JsonElement el) && el.GetBoolean(),
+ Region = s3conf.GetPropString("region"),
+ };
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs
new file mode 100644
index 0000000..85b0b6d
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs
@@ -0,0 +1,47 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: AsyncIntervalAttribute.cs
+*
+* AsyncIntervalAttribute.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Loading.Events
+{
+ /// <summary>
+ /// When added to a method schedules it as a callback on a specified interval when
+ /// the plugin is loaded, and stops when unloaded
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Method)]
+ public sealed class AsyncIntervalAttribute : Attribute
+ {
+ internal readonly TimeSpan Interval;
+
+ /// <summary>
+ /// Intializes the <see cref="AsyncIntervalAttribute"/> with the specified timeout in milliseconds
+ /// </summary>
+ /// <param name="milliseconds">The interval in milliseconds</param>
+ public AsyncIntervalAttribute(int milliseconds)
+ {
+ Interval = TimeSpan.FromMilliseconds(milliseconds);
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/ConfigurableAsyncIntervalAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/ConfigurableAsyncIntervalAttribute.cs
new file mode 100644
index 0000000..12c5ec4
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/ConfigurableAsyncIntervalAttribute.cs
@@ -0,0 +1,51 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: ConfigurableAsyncIntervalAttribute.cs
+*
+* ConfigurableAsyncIntervalAttribute.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Loading.Events
+{
+ /// <summary>
+ /// When added to a method schedules it as a callback on a specified interval when
+ /// the plugin is loaded, and stops when unloaded
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Method)]
+ public sealed class ConfigurableAsyncIntervalAttribute : Attribute
+ {
+ internal readonly string IntervalPropertyName;
+ internal readonly IntervalResultionType Resolution;
+
+ /// <summary>
+ /// Initializes a <see cref="ConfigurableAsyncIntervalAttribute"/> with the specified
+ /// interval property name
+ /// </summary>
+ /// <param name="configPropName">The configuration property name for the event interval</param>
+ /// <param name="resolution">The time resoltion for the event interval</param>
+ public ConfigurableAsyncIntervalAttribute(string configPropName, IntervalResultionType resolution)
+ {
+ IntervalPropertyName = configPropName;
+ Resolution = resolution;
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs
new file mode 100644
index 0000000..f671b07
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs
@@ -0,0 +1,124 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: EventManagment.cs
+*
+* EventManagment.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils.Logging;
+
+namespace VNLib.Plugins.Extensions.Loading.Events
+{
+
+ /// <summary>
+ /// A deletage to form a method signature for shedulable interval callbacks
+ /// </summary>
+ /// <param name="log">The plugin's default log provider</param>
+ /// <param name="pluginExitToken">The plugin's exit token</param>
+ /// <returns>A task the represents the asynchronous work</returns>
+ public delegate Task AsyncSchedulableCallback(ILogProvider log, CancellationToken pluginExitToken);
+
+ /// <summary>
+ /// Provides event schedueling extensions for plugins
+ /// </summary>
+ public static class EventManagment
+ {
+ /// <summary>
+ /// Schedules an asynchronous event interval for the current plugin, that is active until canceled or until the plugin unloads
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="asyncCallback">An asyncrhonous callback method.</param>
+ /// <param name="interval">The event interval</param>
+ /// <param name="immediate">A value that indicates if the callback should be run as soon as possible</param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <remarks>If exceptions are raised during callback execution, they are written to the plugin's default log provider</remarks>
+ public static void ScheduleInterval(this PluginBase plugin, AsyncSchedulableCallback asyncCallback, TimeSpan interval, bool immediate = false)
+ {
+ plugin.ThrowIfUnloaded();
+
+ plugin.Log.Verbose("Interval for {t} scheduled", interval);
+
+ //Run interval on plugins bg scheduler
+ _ = plugin.DeferTask(() => RunIntervalOnPluginScheduler(plugin, asyncCallback, interval, immediate));
+ }
+
+ private static async Task RunIntervalOnPluginScheduler(PluginBase plugin, AsyncSchedulableCallback callback, TimeSpan interval, bool immediate)
+ {
+
+ static async Task RunCallbackAsync(PluginBase plugin, AsyncSchedulableCallback callback)
+ {
+ try
+ {
+ //invoke interval callback
+ await callback(plugin.Log, plugin.UnloadToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ //unloaded
+ plugin.Log.Verbose("Interval callback canceled due to plugin unload or other event cancellation");
+ }
+ catch (Exception ex)
+ {
+ plugin.Log.Error(ex, "Unhandled exception raised during timer callback");
+ }
+ }
+
+ //Run callback immediatly if requested
+ if (immediate)
+ {
+ await RunCallbackAsync(plugin, callback);
+ }
+
+ //Timer loop
+ while (true)
+ {
+ try
+ {
+ //await delay and wait for plugin cancellation
+ await Task.Delay(interval, plugin.UnloadToken);
+ }
+ catch (TaskCanceledException)
+ {
+ //Unload token canceled, exit loop
+ break;
+ }
+
+ await RunCallbackAsync(plugin, callback);
+ }
+ }
+
+ /// <summary>
+ /// Registers an <see cref="IIntervalScheduleable"/> type's event handler for
+ /// raising timed interval events
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="scheduleable">The instance to schedule for timeouts</param>
+ /// <param name="interval">The timeout interval</param>
+ /// <param name="immediate">A value that indicates if the callback should be run as soon as possible</param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <remarks>If exceptions are raised during callback execution, they are written to the plugin's default log provider</remarks>
+ public static void ScheduleInterval(this PluginBase plugin, IIntervalScheduleable scheduleable, TimeSpan interval, bool immediate = false) =>
+ ScheduleInterval(plugin, scheduleable.OnIntervalAsync, interval, immediate);
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/IIntervalScheduleable.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/IIntervalScheduleable.cs
new file mode 100644
index 0000000..5ff40f4
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/IIntervalScheduleable.cs
@@ -0,0 +1,45 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: IIntervalScheduleable.cs
+*
+* IIntervalScheduleable.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils.Logging;
+
+namespace VNLib.Plugins.Extensions.Loading.Events
+{
+ /// <summary>
+ /// Exposes a type for asynchronous event schelueling
+ /// </summary>
+ public interface IIntervalScheduleable
+ {
+ /// <summary>
+ /// A method that is called when the interval time has elapsed
+ /// </summary>
+ /// <param name="log">The plugin default log provider</param>
+ /// <param name="cancellationToken">A token that may cancel an operations if the plugin becomes unloaded</param>
+ /// <returns>A task that resolves when the async operation completes</returns>
+ Task OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken);
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/IntervalResultionType.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/IntervalResultionType.cs
new file mode 100644
index 0000000..d82efc4
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/IntervalResultionType.cs
@@ -0,0 +1,49 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: IntervalResultionType.cs
+*
+* IntervalResultionType.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Plugins.Extensions.Loading.Events
+{
+ /// <summary>
+ /// The configurable event interval resulution type
+ /// </summary>
+ public enum IntervalResultionType
+ {
+ /// <summary>
+ /// Specifies event interval resolution in milliseconds
+ /// </summary>
+ Milliseconds,
+ /// <summary>
+ /// Specifies event interval resolution in seconds
+ /// </summary>
+ Seconds,
+ /// <summary>
+ /// Specifies event interval resolution in minutes
+ /// </summary>
+ Minutes,
+ /// <summary>
+ /// Specifies event interval resolution in hours
+ /// </summary>
+ Hours
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
new file mode 100644
index 0000000..c23f5e2
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
@@ -0,0 +1,334 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: LoadingExtensions.cs
+*
+* LoadingExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Runtime.CompilerServices;
+
+using VNLib.Utils;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Essentials.Accounts;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// Provides common loading (and unloading when required) extensions for plugins
+ /// </summary>
+ public static class LoadingExtensions
+ {
+ public const string DEBUG_CONFIG_KEY = "debug";
+ public const string SECRETS_CONFIG_KEY = "secrets";
+ public const string PASSWORD_HASHING_KEY = "passwords";
+
+ /*
+ * Plugin local cache used for storing singletons for a plugin instance
+ */
+ private static readonly ConditionalWeakTable<PluginBase, PluginLocalCache> _localCache = new();
+
+ /// <summary>
+ /// Gets a previously cached service singleton for the desired plugin
+ /// </summary>
+ /// <param name="serviceType">The service instance type</param>
+ /// <param name="plugin">The plugin to obtain or build the singleton for</param>
+ /// <param name="serviceFactory">The method to produce the singleton</param>
+ /// <returns>The cached or newly created singleton</returns>
+ public static object GetOrCreateSingleton(PluginBase plugin, Type serviceType, Func<PluginBase, object> serviceFactory)
+ {
+ Lazy<object>? service;
+ //Get local cache
+ PluginLocalCache pc = _localCache.GetValue(plugin, PluginLocalCache.Create);
+ //Hold lock while get/set the singleton
+ lock (pc.SyncRoot)
+ {
+ //Check if service already exists
+ service = pc.GetService(serviceType);
+ //publish the service if it isnt loaded yet
+ service ??= pc.AddService(serviceType, serviceFactory);
+ }
+ //Deferred load of the service
+ return service.Value;
+ }
+
+ /// <summary>
+ /// Gets a previously cached service singleton for the desired plugin
+ /// or creates a new singleton instance for the plugin
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="plugin">The plugin to obtain or build the singleton for</param>
+ /// <param name="serviceFactory">The method to produce the singleton</param>
+ /// <returns>The cached or newly created singleton</returns>
+ public static T GetOrCreateSingleton<T>(PluginBase plugin, Func<PluginBase, T> serviceFactory)
+ => (T)GetOrCreateSingleton(plugin, typeof(T), p => serviceFactory(p)!);
+
+
+ /// <summary>
+ /// Gets the plugins ambient <see cref="PasswordHashing"/> if loaded, or loads it if required. This class will
+ /// be unloaded when the plugin us unloaded.
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The ambient <see cref="PasswordHashing"/></returns>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static PasswordHashing GetPasswords(this PluginBase plugin)
+ {
+ plugin.ThrowIfUnloaded();
+ //Get/load the passwords one time only
+ return GetOrCreateSingleton(plugin, LoadPasswords);
+ }
+
+ private static PasswordHashing LoadPasswords(PluginBase plugin)
+ {
+ PasswordHashing Passwords;
+ //Get the global password system secret (pepper)
+ byte[] pepper = plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY)
+ .ToBase64Bytes().Result ?? throw new KeyNotFoundException($"Missing required key '{PASSWORD_HASHING_KEY}' in secrets");
+
+ ERRNO cb(Span<byte> buffer)
+ {
+ //No longer valid peper if plugin is unloaded as its set to zero, so we need to protect it
+ plugin.ThrowIfUnloaded();
+
+ pepper.CopyTo(buffer);
+ return pepper.Length;
+ }
+
+ //See hashing params are defined
+ IReadOnlyDictionary<string, JsonElement>? hashingArgs = plugin.TryGetConfig(PASSWORD_HASHING_KEY);
+ if (hashingArgs != null)
+ {
+ //Get hashing arguments
+ uint saltLen = hashingArgs["salt_len"].GetUInt32();
+ uint hashLen = hashingArgs["hash_len"].GetUInt32();
+ uint timeCost = hashingArgs["time_cost"].GetUInt32();
+ uint memoryCost = hashingArgs["memory_cost"].GetUInt32();
+ uint parallelism = hashingArgs["parallelism"].GetUInt32();
+ //Load passwords
+ Passwords = new(cb, pepper.Length, (int)saltLen, timeCost, memoryCost, parallelism, hashLen);
+ }
+ else
+ {
+ //Init default password hashing
+ Passwords = new(cb, pepper.Length);
+ }
+
+ //Register event to cleanup the password class
+ _ = plugin.RegisterForUnload(() =>
+ {
+ //Zero the pepper
+ CryptographicOperations.ZeroMemory(pepper);
+ });
+ //return
+ return Passwords;
+ }
+
+
+ /// <summary>
+ /// Loads an assembly into the current plugins AppDomain and will unload when disposed
+ /// or the plugin is unloaded from the host application.
+ /// </summary>
+ /// <typeparam name="T">The desired exported type to load from the assembly</typeparam>
+ /// <param name="plugin"></param>
+ /// <param name="assemblyName">The name of the assembly (ex: 'file.dll') to search for</param>
+ /// <param name="dirSearchOption">Directory/file search option</param>
+ /// <returns>The <see cref="AssemblyLoader{T}"/> managing the loaded assmbly in the current AppDomain</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="FileNotFoundException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public static AssemblyLoader<T> LoadAssembly<T>(this PluginBase plugin, string assemblyName, SearchOption dirSearchOption = SearchOption.AllDirectories)
+ {
+ plugin.ThrowIfUnloaded();
+ _ = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName));
+
+ //get plugin directory from config
+ IReadOnlyDictionary<string, JsonElement> config = plugin.GetConfig("plugins");
+ string? pluginsBaseDir = config["path"].GetString();
+
+ /*
+ * This should never happen since this method can only be called from a
+ * plugin context, which means this path was used to load the current plugin
+ */
+ _ = pluginsBaseDir ?? throw new ArgumentNullException("path", "No plugin path is defined for the current host configuration, this is likely a bug");
+
+ //Get the first file that matches the search file
+ string? asmFile = Directory.EnumerateFiles(pluginsBaseDir, assemblyName, dirSearchOption).FirstOrDefault();
+ _ = asmFile ?? throw new FileNotFoundException($"Failed to load custom assembly {assemblyName} from plugin directory");
+
+ //Load the assembly
+ return AssemblyLoader<T>.Load(asmFile, plugin.UnloadToken);
+ }
+
+
+ /// <summary>
+ /// Determintes if the current plugin config has a debug propety set
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>True if debug mode is enabled, false otherwise</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static bool IsDebug(this PluginBase plugin)
+ {
+ plugin.ThrowIfUnloaded();
+ //Check for debug element
+ return plugin.PluginConfig.TryGetProperty(DEBUG_CONFIG_KEY, out JsonElement dbgEl) && dbgEl.GetBoolean();
+ }
+
+ /// <summary>
+ /// Internal exception helper to raise <see cref="ObjectDisposedException"/> if the plugin has been unlaoded
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static void ThrowIfUnloaded(this PluginBase plugin)
+ {
+ //See if the plugin was unlaoded
+ if (plugin.UnloadToken.IsCancellationRequested)
+ {
+ throw new ObjectDisposedException("The plugin has been unloaded");
+ }
+ }
+
+ /// <summary>
+ /// Schedules an asynchronous callback function to run and its results will be observed
+ /// when the operation completes, or when the plugin is unloading
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="asyncTask">The asynchronous operation to observe</param>
+ /// <param name="delayMs">An optional startup delay for the operation</param>
+ /// <returns>A task that completes when the deferred task completes </returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static async Task DeferTask(this PluginBase plugin, Func<Task> asyncTask, int delayMs = 0)
+ {
+ /*
+ * Motivation:
+ * Sometimes during plugin loading, a plugin may want to asynchronously load
+ * data, where the results are not required to be observed during loading, but
+ * should not be pending after the plugin is unloaded, as the assembly may be
+ * unloaded and referrences collected by the GC.
+ *
+ * So we can use the plugin's unload cancellation token to observe the results
+ * of a pending async operation
+ */
+
+ //Test status
+ plugin.ThrowIfUnloaded();
+
+ //Optional delay
+ await Task.Delay(delayMs);
+
+ //Run on ts
+ Task deferred = Task.Run(asyncTask);
+
+ //Add task to deferred list
+ plugin.ObserveTask(deferred);
+ try
+ {
+ //Await the task results
+ await deferred.ConfigureAwait(false);
+ }
+ catch(Exception ex)
+ {
+ //Log errors
+ plugin.Log.Error(ex, "Error occured while observing deferred task");
+ }
+ finally
+ {
+ //Remove task when complete
+ plugin.RemoveObservedTask(deferred);
+ }
+ }
+
+ /// <summary>
+ /// Registers an event to occur when the plugin is unloaded on a background thread
+ /// and will cause the Plugin.Unload() method to block until the event completes
+ /// </summary>
+ /// <param name="pbase"></param>
+ /// <param name="callback">The method to call when the plugin is unloaded</param>
+ /// <returns>A task that represents the registered work</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static Task RegisterForUnload(this PluginBase pbase, Action callback)
+ {
+ //Test status
+ pbase.ThrowIfUnloaded();
+ _ = callback ?? throw new ArgumentNullException(nameof(callback));
+
+ //Wait method
+ static async Task WaitForUnload(PluginBase pb, Action callback)
+ {
+ //Wait for unload as a task on the threadpool to avoid deadlocks
+ await pb.UnloadToken.WaitHandle.WaitAsync()
+ .ConfigureAwait(false);
+
+ callback();
+ }
+
+ //Registaer the task to cause the plugin to wait
+ return pbase.DeferTask(() => WaitForUnload(pbase, callback));
+ }
+
+
+ private sealed class PluginLocalCache
+ {
+ private readonly PluginBase _plugin;
+
+ private readonly Dictionary<Type, Lazy<object>> _store;
+
+ public object SyncRoot { get; } = new();
+
+ private PluginLocalCache(PluginBase plugin)
+ {
+ _plugin = plugin;
+ _store = new();
+ //Register cleanup on unload
+ _ = _plugin.RegisterForUnload(() => _store.Clear());
+ }
+
+ public static PluginLocalCache Create(PluginBase plugin) => new(plugin);
+
+
+ public Lazy<object>? GetService(Type serviceType)
+ {
+ Lazy<object>? t = _store.Where(t => t.Key.IsAssignableTo(serviceType))
+ .Select(static tk => tk.Value)
+ .FirstOrDefault();
+ return t;
+ }
+
+ public Lazy<object> AddService(Type serviceType, Func<PluginBase, object> factory)
+ {
+ //Get lazy loader to invoke factory outside of cache lock
+ Lazy<object> lazyFactory = new(() => factory(_plugin), true);
+ //Store lazy factory
+ _store.Add(serviceType, lazyFactory);
+ //Pass the lazy factory back
+ return lazyFactory;
+ }
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs b/lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs
new file mode 100644
index 0000000..336f6a4
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs
@@ -0,0 +1,102 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: PrivateKey.cs
+*
+* PrivateKey.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Text;
+using System.Security.Cryptography;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// A container for a PKSC#8 encoed private key
+ /// </summary>
+ public sealed class PrivateKey : VnDisposeable
+ {
+ private readonly byte[] _utf8RawData;
+
+ /// <summary>
+ /// Decodes the PKCS#8 encoded private key from a secret, as an EC private key
+ /// and recovers the ECDsa algorithm from the key
+ /// </summary>
+ /// <returns>The <see cref="ECDsa"/> algoritm from the private key</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="CryptographicException"></exception>
+ public ECDsa GetECDsa()
+ {
+ //Alloc buffer
+ using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(_utf8RawData.Length);
+ //Get base64 bytes from utf8
+ ERRNO count = VnEncoding.Base64UrlDecode(_utf8RawData, buffer.Span);
+ //Parse the private key
+ ECDsa alg = ECDsa.Create();
+ alg.ImportPkcs8PrivateKey(buffer.Span[..(int)count], out _);
+ //Wipe the buffer
+ Memory.InitializeBlock(buffer.Span);
+ return alg;
+ }
+
+ /// <summary>
+ /// Decodes the PKCS#8 encoded private key from a secret, as an RSA private key
+ /// </summary>
+ /// <returns>The <see cref="RSA"/> algorithm from the private key</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="CryptographicException"></exception>
+ public RSA GetRSA()
+ {
+ //Alloc buffer
+ using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(_utf8RawData.Length);
+ //Get base64 bytes from utf8
+ ERRNO count = VnEncoding.Base64UrlDecode(_utf8RawData, buffer.Span);
+ //Parse the private key
+ RSA alg = RSA.Create();
+ alg.ImportPkcs8PrivateKey(buffer.Span[..(int)count], out _);
+ //Wipe the buffer
+ Memory.InitializeBlock(buffer.Span);
+ return alg;
+ }
+
+ internal PrivateKey(SecretResult secret)
+ {
+ //Alloc and get utf8
+ byte[] buffer = new byte[secret.Result.Length];
+ int count = Encoding.UTF8.GetBytes(secret.Result, buffer);
+ //Verify length
+ if(count != buffer.Length)
+ {
+ throw new FormatException("UTF8 deocde failed");
+ }
+ //Store
+ _utf8RawData = buffer;
+ }
+
+ protected override void Free()
+ {
+ Memory.InitializeBlock(_utf8RawData.AsSpan());
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs
new file mode 100644
index 0000000..9242522
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs
@@ -0,0 +1,161 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: RoutingExtensions.cs
+*
+* RoutingExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Text.Json;
+using System.Reflection;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+using VNLib.Plugins.Extensions.Loading.Events;
+
+namespace VNLib.Plugins.Extensions.Loading.Routing
+{
+ /// <summary>
+ /// Provides advanced QOL features to plugin loading
+ /// </summary>
+ public static class RoutingExtensions
+ {
+ private static readonly ConditionalWeakTable<IEndpoint, PluginBase?> _pluginRefs = new();
+
+ /// <summary>
+ /// Constructs and routes the specific endpoint type for the current plugin
+ /// </summary>
+ /// <typeparam name="T">The <see cref="IEndpoint"/> type</typeparam>
+ /// <param name="plugin"></param>
+ /// <param name="pluginConfigPathName">The path to the plugin sepcific configuration property</param>
+ /// <exception cref="TargetInvocationException"></exception>
+ public static T Route<T>(this PluginBase plugin, string? pluginConfigPathName) where T : IEndpoint
+ {
+ Type endpointType = typeof(T);
+ //If the config attribute is not set, then ignore the config variables
+ if (string.IsNullOrWhiteSpace(pluginConfigPathName))
+ {
+ ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase) });
+ _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}");
+ //Create the new endpoint and pass the plugin instance
+ T endpoint = (T)constructor.Invoke(new object[] { plugin });
+ //Register event handlers for the endpoint
+ ScheduleIntervals(plugin, endpoint, endpointType, null);
+ //Route the endpoint
+ plugin.Route(endpoint);
+
+ //Store ref to plugin for endpoint
+ _pluginRefs.Add(endpoint, plugin);
+
+ return endpoint;
+ }
+ else
+ {
+ ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IReadOnlyDictionary<string, JsonElement>) });
+ //Make sure the constructor exists
+ _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}");
+ //Get config variables for the endpoint
+ IReadOnlyDictionary<string, JsonElement> conf = plugin.GetConfig(pluginConfigPathName);
+ //Create the new endpoint and pass the plugin instance along with the configuration object
+ T endpoint = (T)constructor.Invoke(new object[] { plugin, conf });
+ //Register event handlers for the endpoint
+ ScheduleIntervals(plugin, endpoint, endpointType, conf);
+ //Route the endpoint
+ plugin.Route(endpoint);
+
+ //Store ref to plugin for endpoint
+ _pluginRefs.Add(endpoint, plugin);
+
+ return endpoint;
+ }
+ }
+
+ /// <summary>
+ /// Constructs and routes the specific endpoint type for the current plugin
+ /// </summary>
+ /// <typeparam name="T">The <see cref="IEndpoint"/> type</typeparam>
+ /// <param name="plugin"></param>
+ /// <exception cref="TargetInvocationException"></exception>
+ public static T Route<T>(this PluginBase plugin) where T : IEndpoint
+ {
+ Type endpointType = typeof(T);
+ //Get config name attribute
+ ConfigurationNameAttribute? configAttr = endpointType.GetCustomAttribute<ConfigurationNameAttribute>();
+ //Route using attribute
+ return plugin.Route<T>(configAttr?.ConfigVarName);
+ }
+
+ /// <summary>
+ /// Gets the plugin that loaded the current endpoint
+ /// </summary>
+ /// <param name="ep"></param>
+ /// <returns>The plugin that loaded the current endpoint</returns>
+ /// <exception cref="InvalidOperationException"></exception>
+ public static PluginBase GetPlugin(this IEndpoint ep)
+ {
+ _ = _pluginRefs.TryGetValue(ep, out PluginBase? pBase);
+ return pBase ?? throw new InvalidOperationException("Endpoint was not dynamically routed");
+ }
+
+ private static void ScheduleIntervals<T>(PluginBase plugin, T endpointInstance, Type epType, IReadOnlyDictionary<string, JsonElement>? endpointLocalConfig) where T : IEndpoint
+ {
+ //Get all methods that have the configureable async interval attribute specified
+ IEnumerable<Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback>> confIntervals = epType.GetMethods()
+ .Where(m => m.GetCustomAttribute<ConfigurableAsyncIntervalAttribute>() != null)
+ .Select(m => new Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback>
+ (m.GetCustomAttribute<ConfigurableAsyncIntervalAttribute>()!, m.CreateDelegate<AsyncSchedulableCallback>(endpointInstance)));
+
+ //If the endpoint has a local config, then use it to find the interval
+ if (endpointLocalConfig != null)
+ {
+
+ //Schedule event handlers on the current plugin
+ foreach (Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback> interval in confIntervals)
+ {
+ int value = endpointLocalConfig[interval.Item1.IntervalPropertyName].GetInt32();
+ //Get the timeout from its resolution variable
+ TimeSpan timeout = interval.Item1.Resolution switch
+ {
+ IntervalResultionType.Seconds => TimeSpan.FromSeconds(value),
+ IntervalResultionType.Minutes => TimeSpan.FromMinutes(value),
+ IntervalResultionType.Hours => TimeSpan.FromHours(value),
+ _ => TimeSpan.FromMilliseconds(value),
+ };
+ //Schedule
+ plugin.ScheduleInterval(interval.Item2, timeout);
+ }
+ }
+
+ //Get all methods that have the async interval attribute specified
+ IEnumerable<Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback>> intervals = epType.GetMethods()
+ .Where(m => m.GetCustomAttribute<AsyncIntervalAttribute>() != null)
+ .Select(m => new Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback>(
+ m.GetCustomAttribute<AsyncIntervalAttribute>()!, m.CreateDelegate<AsyncSchedulableCallback>(endpointInstance))
+ );
+
+ //Schedule event handlers on the current plugin
+ foreach (Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback> interval in intervals)
+ {
+ plugin.ScheduleInterval(interval.Item2, interval.Item1.Interval);
+ }
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs b/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs
new file mode 100644
index 0000000..76c2bc2
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs
@@ -0,0 +1,35 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: S3Config.cs
+*
+* S3Config.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ public sealed class S3Config
+ {
+ public string? ServerAddress { get; init; }
+ public string? ClientId { get; init; }
+ public string? BaseBucket { get; init; }
+ public bool? UseSsl { get; init; }
+ public string? Region { get; init; }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs b/lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs
new file mode 100644
index 0000000..15323f3
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs
@@ -0,0 +1,61 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: SecretResult.cs
+*
+* SecretResult.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+using VNLib.Utils;
+using VNLib.Utils.Extensions;
+using VNLib.Utils.Memory;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// The result of a secret fetch operation
+ /// </summary>
+ public sealed class SecretResult : VnDisposeable
+ {
+ private readonly char[] _secretChars;
+
+ /// <summary>
+ /// The protected raw result value
+ /// </summary>
+ public ReadOnlySpan<char> Result => _secretChars;
+
+
+ internal SecretResult(ReadOnlySpan<char> value) => _secretChars = value.ToArray();
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ Memory.InitializeBlock(_secretChars.AsSpan());
+ }
+
+ internal static SecretResult ToSecret(string? result)
+ {
+ SecretResult res = new(result.AsSpan());
+ Memory.UnsafeZeroMemory<char>(result);
+ return res;
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs b/lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs
new file mode 100644
index 0000000..da090ec
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs
@@ -0,0 +1,93 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: UserLoading.cs
+*
+* UserLoading.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Collections.Generic;
+
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Essentials.Users;
+
+namespace VNLib.Plugins.Extensions.Loading.Users
+{
+ /// <summary>
+ /// Contains extension methods for plugins to load the "users" system
+ /// </summary>
+ public static class UserLoading
+ {
+ public const string USER_CUSTOM_ASSEMBLY = "user_custom_asm";
+ public const string DEFAULT_USER_ASM = "VNLib.Plugins.Essentials.Users.dll";
+ public const string ONLOAD_METHOD_NAME = "OnPluginLoading";
+
+
+ /// <summary>
+ /// Gets or loads the plugin's ambient <see cref="IUserManager"/>, with the specified user-table name,
+ /// or the default table name
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The ambient <see cref="IUserManager"/> for the current plugin</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IUserManager GetUserManager(this PluginBase plugin)
+ {
+ plugin.ThrowIfUnloaded();
+ //Get stored or load
+ return LoadingExtensions.GetOrCreateSingleton(plugin, LoadUsers);
+ }
+
+ private static IUserManager LoadUsers(PluginBase pbase)
+ {
+ //Try to load a custom user assembly for exporting IUserManager
+ string? customAsm = pbase.PluginConfig.GetPropString(USER_CUSTOM_ASSEMBLY);
+ //See if host config defined the path
+ customAsm ??= pbase.HostConfig.GetPropString(USER_CUSTOM_ASSEMBLY);
+ //Finally default
+ customAsm ??= DEFAULT_USER_ASM;
+
+ //Try to load a custom assembly
+ AssemblyLoader<IUserManager> loader = pbase.LoadAssembly<IUserManager>(customAsm);
+ try
+ {
+ //Try to get the onload method
+ Action<object>? onLoadMethod = loader.TryGetMethod<Action<object>>(ONLOAD_METHOD_NAME);
+
+ //Call the onplugin load method
+ onLoadMethod?.Invoke(pbase);
+
+ if (pbase.IsDebug())
+ {
+ pbase.Log.Verbose("Loading user manager from assembly {name}", loader.Resource.GetType().AssemblyQualifiedName);
+ }
+
+ //Return the loaded instance (may raise exception)
+ return loader.Resource;
+ }
+ catch
+ {
+ loader.Dispose();
+ throw;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj
new file mode 100644
index 0000000..15bb15e
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj
@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <Authors>Vaughn Nugent</Authors>
+ <Version>1.0.1.1</Version>
+
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <Nullable>enable</Nullable>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
+ <PackageReference Include="VaultSharp" Version="1.7.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\VNLib\Essentials\src\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\PluginBase\VNLib.Plugins.PluginBase.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.xml b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.xml
new file mode 100644
index 0000000..963f506
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.xml
@@ -0,0 +1,260 @@
+<?xml version="1.0"?>
+<!--
+Copyright (c) 2022 Vaughn Nugent
+-->
+<doc>
+ <assembly>
+ <name>VNLib.Plugins.Extensions.Loading</name>
+ </assembly>
+ <members>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute">
+ <summary>
+ Specifies a configuration variable name in the plugin's configuration
+ containing data specific to the type
+ </summary>
+ </member>
+ <member name="F:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute.ConfigVarName">
+ <summary>
+
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute.#ctor(System.String)">
+ <summary>
+ Initializes a new <see cref="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute"/>
+ </summary>
+ <param name="configVarName">The name of the configuration variable for the class</param>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions">
+ <summary>
+ Contains extensions for plugin configuration specifc extensions
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfigForType``1(VNLib.Plugins.PluginBase)">
+ <summary>
+ Retrieves a top level configuration dictionary of elements for the specified type.
+ The type must contain a <see cref="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute"/>
+ </summary>
+ <typeparam name="T">The type to get the configuration of</typeparam>
+ <param name="plugin"></param>
+ <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfig(VNLib.Plugins.PluginBase,System.String)">
+ <summary>
+ Retrieves a top level configuration dictionary of elements with the specified property name,
+ from the plugin config first, or falls back to the host config file
+ </summary>
+ <param name="plugin"></param>
+ <param name="propName">The config property name to retrieve</param>
+ <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.TryGetConfig(VNLib.Plugins.PluginBase,System.String)">
+ <summary>
+ Retrieves a top level configuration dictionary of elements with the specified property name,
+ from the plugin config first, or falls back to the host config file
+ </summary>
+ <param name="plugin"></param>
+ <param name="propName">The config property name to retrieve</param>
+ <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfigForType(VNLib.Plugins.PluginBase,System.Type)">
+ <summary>
+ Retrieves a top level configuration dictionary of elements for the specified type.
+ The type must contain a <see cref="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute"/>
+ </summary>
+ <param name="plugin"></param>
+ <param name="type">The type to get configuration data for</param>
+ <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfig(VNLib.Plugins.PluginBase,System.Object)">
+ <summary>
+ Shortcut extension for <see cref="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfigForType``1(VNLib.Plugins.PluginBase)"/> to get
+ config of current class
+ </summary>
+ <param name="obj">The object that a configuration can be retrieved for</param>
+ <param name="plugin">The plugin containing configuration variables</param>
+ <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.HasConfigForType``1(VNLib.Plugins.PluginBase)">
+ <summary>
+ Determines if the current plugin configuration contains the require properties to initialize
+ the type
+ </summary>
+ <typeparam name="T"></typeparam>
+ <param name="plugin"></param>
+ <returns>True if the plugin config contains the require configuration property</returns>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Events.AsyncIntervalAttribute">
+ <summary>
+ When added to a method schedules it as a callback on a specified interval when
+ the plugin is loaded, and stops when unloaded
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Events.AsyncIntervalAttribute.#ctor(System.Int32)">
+ <summary>
+ Intializes the <see cref="T:VNLib.Plugins.Extensions.Loading.Events.AsyncIntervalAttribute"/> with the specified timeout in milliseconds
+ </summary>
+ <param name="milliseconds">The interval in milliseconds</param>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType">
+ <summary>
+ The configurable event interval resulution type
+ </summary>
+ </member>
+ <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Milliseconds">
+ <summary>
+ Specifies event interval resolution in milliseconds
+ </summary>
+ </member>
+ <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Seconds">
+ <summary>
+ Specifies event interval resolution in seconds
+ </summary>
+ </member>
+ <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Minutes">
+ <summary>
+ Specifies event interval resolution in minutes
+ </summary>
+ </member>
+ <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Hours">
+ <summary>
+ Specifies event interval resolution in hours
+ </summary>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Events.ConfigurableAsyncIntervalAttribute">
+ <summary>
+ When added to a method schedules it as a callback on a specified interval when
+ the plugin is loaded, and stops when unloaded
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Events.ConfigurableAsyncIntervalAttribute.#ctor(System.String,VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType)">
+ <summary>
+ Initializes a <see cref="T:VNLib.Plugins.Extensions.Loading.Events.ConfigurableAsyncIntervalAttribute"/> with the specified
+ interval property name
+ </summary>
+ <param name="configPropName">The configuration property name for the event interval</param>
+ <param name="resolution">The time resoltion for the event interval</param>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Events.EventHandle">
+ <summary>
+ Represents a handle to a scheduled event interval that is managed by the plugin but may be cancled by disposing the instance
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Events.EventHandle.Pause">
+ <summary>
+ Pauses the event timer until the <see cref="T:VNLib.Utils.OpenHandle"/> is released or disposed
+ then resumes to the inital interval period
+ </summary>
+ <returns>A <see cref="T:VNLib.Utils.OpenHandle"/> that restores the timer to its initial state when disposed</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Events.EventHandle.Free">
+ <inheritdoc/>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Events.EventManagment">
+ <summary>
+ Provides event schedueling extensions for plugins
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Events.EventManagment.ScheduleInterval``1(VNLib.Plugins.PluginBase,System.Func{``0,System.Threading.Tasks.Task},``0,System.TimeSpan)">
+ <summary>
+ Schedules an asynchronous event interval for the current plugin, that is active until canceled or until the plugin unloads
+ </summary>
+ <typeparam name="TState">Stateful event argument</typeparam>
+ <param name="plugin"></param>
+ <param name="asyncCallback">An asyncrhonous callback method.</param>
+ <param name="state"></param>
+ <param name="interval">The event interval</param>
+ <returns>An <see cref="T:VNLib.Plugins.Extensions.Loading.Events.EventHandle"/> that can manage the interval state</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ <remarks>If exceptions are raised during callback execution, they are written to the plugin's default log provider</remarks>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.LoadingExtensions">
+ <summary>
+ Provides common loading (and unloading when required) extensions for plugins
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.GetPasswords(VNLib.Plugins.PluginBase)">
+ <summary>
+ Gets the plugins ambient <see cref="T:VNLib.Plugins.Essentials.Accounts.PasswordHashing"/> if loaded, or loads it if required. This class will
+ be unloaded when the plugin us unloaded.
+ </summary>
+ <param name="plugin"></param>
+ <returns>The ambient <see cref="T:VNLib.Plugins.Essentials.Accounts.PasswordHashing"/></returns>
+ <exception cref="T:System.OverflowException"></exception>
+ <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.GetUserManager(VNLib.Plugins.PluginBase)">
+ <summary>
+ Gets or loads the plugin's ambient <see cref="T:VNLib.Plugins.Essentials.Users.UserManager"/>, with the specified user-table name,
+ or the default table name
+ </summary>
+ <param name="plugin"></param>
+ <returns>The ambient <see cref="T:VNLib.Plugins.Essentials.Users.UserManager"/> for the current plugin</returns>
+ <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.IsDebug(VNLib.Plugins.PluginBase)">
+ <summary>
+ Determintes if the current plugin config has a debug propety set
+ </summary>
+ <param name="plugin"></param>
+ <returns>True if debug mode is enabled, false otherwise</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.ThrowIfUnloaded(VNLib.Plugins.PluginBase)">
+ <summary>
+ Internal exception helper to raise <see cref="T:System.ObjectDisposedException"/> if the plugin has been unlaoded
+ </summary>
+ <param name="plugin"></param>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Routing.RoutingExtensions.Route``1(VNLib.Plugins.PluginBase,System.String)">
+ <summary>
+ Constructs and routes the specific endpoint type for the current plugin
+ </summary>
+ <typeparam name="T">The <see cref="T:VNLib.Plugins.IEndpoint"/> type</typeparam>
+ <param name="plugin"></param>
+ <param name="pluginConfigPathName">The path to the plugin sepcific configuration property</param>
+ <exception cref="T:System.Reflection.TargetInvocationException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Routing.RoutingExtensions.Route``1(VNLib.Plugins.PluginBase)">
+ <summary>
+ Constructs and routes the specific endpoint type for the current plugin
+ </summary>
+ <typeparam name="T">The <see cref="T:VNLib.Plugins.IEndpoint"/> type</typeparam>
+ <param name="plugin"></param>
+ <exception cref="T:System.Reflection.TargetInvocationException"></exception>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Sql.SqlDbConnectionLoader">
+ <summary>
+ Provides common basic SQL loading extensions for plugins
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Sql.SqlDbConnectionLoader.GetConnectionFactory(VNLib.Plugins.PluginBase)">
+ <summary>
+ Gets (or loads) the ambient sql connection factory for the current plugin
+ </summary>
+ <param name="plugin"></param>
+ <returns>The ambient <see cref="T:System.Data.Common.DbConnection"/> factory</returns>
+ <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Sql.SqlDbConnectionLoader.GetContextOptions(VNLib.Plugins.PluginBase)">
+ <summary>
+ Gets (or loads) the ambient <see cref="T:Microsoft.EntityFrameworkCore.DbContextOptions"/> configured from
+ the ambient sql factory
+ </summary>
+ <param name="plugin"></param>
+ <returns>The ambient <see cref="T:Microsoft.EntityFrameworkCore.DbContextOptions"/> for the current plugin</returns>
+ <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ <remarks>If plugin is in debug mode, writes log data to the default log</remarks>
+ </member>
+ </members>
+</doc>
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs b/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs
new file mode 100644
index 0000000..898d64c
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs
@@ -0,0 +1,473 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: VaultSecrets.cs
+*
+* VaultSecrets.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Security.Cryptography.X509Certificates;
+
+using VaultSharp;
+using VaultSharp.V1.Commons;
+using VaultSharp.V1.AuthMethods;
+using VaultSharp.V1.AuthMethods.Token;
+using VaultSharp.V1.AuthMethods.AppRole;
+using VaultSharp.V1.SecretsEngines.PKI;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Hashing.IdentityUtility;
+using System.Threading;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+
+ /// <summary>
+ /// Adds loading extensions for secure/centralized configuration secrets
+ /// </summary>
+ public static class PluginSecretLoading
+ {
+ public const string VAULT_OBJECT_NAME = "hashicorp_vault";
+ public const string SECRETS_CONFIG_KEY = "secrets";
+ public const string VAULT_TOKEN_KEY = "token";
+ public const string VAULT_ROLE_KEY = "role";
+ public const string VAULT_SECRET_KEY = "secret";
+
+ public const string VAULT_URL_KEY = "url";
+
+ public const string VAULT_URL_SCHEME = "vault://";
+
+
+ /// <summary>
+ /// <para>
+ /// Gets a secret from the "secrets" element.
+ /// </para>
+ /// <para>
+ /// Secrets elements are merged from the host config and plugin local config 'secrets' element.
+ /// before searching. The plugin config takes precedence over the host config.
+ /// </para>
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="secretName">The name of the secret propery to get</param>
+ /// <returns>The element from the configuration file with the given name, or null if the configuration or property does not exist</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static Task<SecretResult?> TryGetSecretAsync(this PluginBase plugin, string secretName)
+ {
+ //Get the secret from the config file raw
+ string? rawSecret = TryGetSecretInternal(plugin, secretName);
+ if (rawSecret == null)
+ {
+ return Task.FromResult<SecretResult?>(null);
+ }
+
+ //Secret is a vault path, or return the raw value
+ if (!rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase))
+ {
+ return Task.FromResult<SecretResult?>(new(rawSecret.AsSpan()));
+ }
+ return GetSecretFromVaultAsync(plugin, rawSecret);
+ }
+
+ /// <summary>
+ /// Gets a secret at the given vault url (in the form of "vault://[mount-name]/[secret-path]?secret=[secret_name]")
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="vaultPath">The raw vault url to lookup</param>
+ /// <returns>The string of the object at the specified vault path</returns>
+ /// <exception cref="UriFormatException"></exception>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static Task<SecretResult?> GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath)
+ {
+ //print the path for debug
+ if (plugin.IsDebug())
+ {
+ plugin.Log.Debug("Retrieving secret {s} from vault", vaultPath.ToString());
+ }
+
+ //Slice off path
+ ReadOnlySpan<char> paq = vaultPath.SliceAfterParam(VAULT_URL_SCHEME);
+ ReadOnlySpan<char> path = paq.SliceBeforeParam('?');
+ ReadOnlySpan<char> query = paq.SliceAfterParam('?');
+
+ if (paq.IsEmpty)
+ {
+ throw new UriFormatException("Vault secret location not valid/empty ");
+ }
+ //Get the secret
+ string secretTableKey = query.SliceAfterParam("secret=").SliceBeforeParam('&').ToString();
+ string vaultType = query.SliceBeforeParam("vault_type=").SliceBeforeParam('&').ToString();
+
+ //get mount and path
+ int lastSep = path.IndexOf('/');
+ string mount = path[..lastSep].ToString();
+ string secret = path[(lastSep + 1)..].ToString();
+
+ async Task<SecretResult?> execute()
+ {
+ //Try load client
+ IVaultClient? client = plugin.GetVault();
+
+ _ = client ?? throw new KeyNotFoundException("Vault client not found");
+ //run read async
+ Secret<SecretData> result = await client.V1.Secrets.KeyValue.V2.ReadSecretAsync(path:secret, mountPoint:mount);
+ //Read the secret
+ return SecretResult.ToSecret(result.Data.Data[secretTableKey].ToString());
+ }
+
+ return Task.Run(execute);
+ }
+
+ /// <summary>
+ /// <para>
+ /// Gets a Certicate from the "secrets" element.
+ /// </para>
+ /// <para>
+ /// Secrets elements are merged from the host config and plugin local config 'secrets' element.
+ /// before searching. The plugin config takes precedence over the host config.
+ /// </para>
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="secretName">The name of the secret propery to get</param>
+ /// <returns>The element from the configuration file with the given name, or null if the configuration or property does not exist</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static Task<X509Certificate?> TryGetCertificateAsync(this PluginBase plugin, string secretName)
+ {
+ //Get the secret from the config file raw
+ string? rawSecret = TryGetSecretInternal(plugin, secretName);
+ if (rawSecret == null)
+ {
+ return Task.FromResult<X509Certificate?>(null);
+ }
+
+ //Secret is a vault path, or return the raw value
+ if (!rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase))
+ {
+ return Task.FromResult<X509Certificate?>(new (rawSecret));
+ }
+ return GetCertFromVaultAsync(plugin, rawSecret);
+ }
+
+ public static Task<X509Certificate?> GetCertFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath, CertificateCredentialsRequestOptions? options = null)
+ {
+ //print the path for debug
+ if (plugin.IsDebug())
+ {
+ plugin.Log.Debug("Retrieving certificate {s} from vault", vaultPath.ToString());
+ }
+
+ //Slice off path
+ ReadOnlySpan<char> paq = vaultPath.SliceAfterParam(VAULT_URL_SCHEME);
+ ReadOnlySpan<char> path = paq.SliceBeforeParam('?');
+ ReadOnlySpan<char> query = paq.SliceAfterParam('?');
+
+ if (paq.IsEmpty)
+ {
+ throw new UriFormatException("Vault secret location not valid/empty ");
+ }
+
+ //Get the secret
+ string role = query.SliceAfterParam("role=").SliceBeforeParam('&').ToString();
+ string vaultType = query.SliceBeforeParam("vault_type=").SliceBeforeParam('&').ToString();
+ string commonName = query.SliceBeforeParam("cn=").SliceBeforeParam('&').ToString();
+
+ //get mount and path
+ int lastSep = path.IndexOf('/');
+ string mount = path[..lastSep].ToString();
+ string secret = path[(lastSep + 1)..].ToString();
+
+ async Task<X509Certificate?> execute()
+ {
+ //Try load client
+ IVaultClient? client = plugin.GetVault();
+
+ _ = client ?? throw new KeyNotFoundException("Vault client not found");
+
+ options ??= new()
+ {
+ CertificateFormat = CertificateFormat.pem,
+ PrivateKeyFormat = PrivateKeyFormat.pkcs8,
+ CommonName = commonName,
+ };
+
+ //run read async
+ Secret<CertificateCredentials> result = await client.V1.Secrets.PKI.GetCredentialsAsync(pkiRoleName:secret, certificateCredentialRequestOptions:options, pkiBackendMountPoint:mount);
+ //Read the secret
+ byte[] pemCertData = Encoding.UTF8.GetBytes(result.Data.CertificateContent);
+
+ return new (pemCertData);
+ }
+
+ return Task.Run(execute);
+ }
+
+ /// <summary>
+ /// Gets the ambient vault client for the current plugin
+ /// if the configuration is loaded, null otherwise
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The ambient <see cref="IVaultClient"/> if loaded, null otherwise</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IVaultClient? GetVault(this PluginBase plugin) => LoadingExtensions.GetOrCreateSingleton(plugin, TryGetVaultLoader);
+
+ private static string? TryGetSecretInternal(PluginBase plugin, string secretName)
+ {
+ bool local = plugin.PluginConfig.TryGetProperty(SECRETS_CONFIG_KEY, out JsonElement localEl);
+ bool host = plugin.HostConfig.TryGetProperty(SECRETS_CONFIG_KEY, out JsonElement hostEl);
+
+ //total config
+ IReadOnlyDictionary<string, JsonElement>? conf;
+
+ if (local && host)
+ {
+ //Load both config objects to dict
+ Dictionary<string, JsonElement> localConf = localEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value);
+ Dictionary<string, JsonElement> hostConf = hostEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value);
+
+ //merge the two configs
+ foreach(KeyValuePair<string, JsonElement> lc in localConf)
+ {
+ //Overwrite any host config keys, plugin conf takes priority
+ hostConf[lc.Key] = lc.Value;
+ }
+ //set the merged config
+ conf = hostConf;
+ }
+ else if(local)
+ {
+ //Store only local config
+ conf = localEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value);
+ }
+ else if(host)
+ {
+ //store only host config
+ conf = hostEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value);
+ }
+ else
+ {
+ conf = null;
+ }
+
+ //Get the value or default json element
+ return conf != null && conf.TryGetValue(secretName, out JsonElement el) ? el.GetString() : null;
+ }
+
+ private static IVaultClient? TryGetVaultLoader(PluginBase pbase)
+ {
+ //Get vault config
+ IReadOnlyDictionary<string, JsonElement>? conf = pbase.TryGetConfig(VAULT_OBJECT_NAME);
+
+ if (conf == null)
+ {
+ return null;
+ }
+
+ //try get servre address creds from config
+ string? serverAddress = conf[VAULT_URL_KEY].GetString() ?? throw new KeyNotFoundException($"Failed to load the key {VAULT_URL_KEY} from object {VAULT_OBJECT_NAME}");
+
+ IAuthMethodInfo authMethod;
+
+ //Get authentication method from config
+ if (conf.TryGetValue(VAULT_TOKEN_KEY, out JsonElement tokenEl))
+ {
+ //Init token
+ authMethod = new TokenAuthMethodInfo(tokenEl.GetString());
+ }
+ else if (conf.TryGetValue(VAULT_ROLE_KEY, out JsonElement roleEl) && conf.TryGetValue(VAULT_SECRET_KEY, out JsonElement secretEl))
+ {
+ authMethod = new AppRoleAuthMethodInfo(roleEl.GetString(), secretEl.GetString());
+ }
+ else
+ {
+ throw new KeyNotFoundException($"Failed to load the vault authentication method from {VAULT_OBJECT_NAME}");
+ }
+
+ //Settings
+ VaultClientSettings settings = new(serverAddress, authMethod);
+
+ //create vault client
+ return new VaultClient(settings);
+ }
+
+ /// <summary>
+ /// Gets the Secret value as a byte buffer
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The base64 decoded secret as a byte[]</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="InternalBufferTooSmallException"></exception>
+ public static byte[] GetFromBase64(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+
+ //Temp buffer
+ using UnsafeMemoryHandle<byte> buffer = Memory.UnsafeAlloc<byte>(secret.Result.Length);
+
+ //Get base64
+ if(Convert.TryFromBase64Chars(secret.Result, buffer, out int count))
+ {
+ //Copy to array
+ byte[] value = buffer.Span[..count].ToArray();
+ //Clear block before returning
+ Memory.InitializeBlock<byte>(buffer);
+
+ return value;
+ }
+
+ throw new InternalBufferTooSmallException("internal buffer too small");
+ }
+
+ /// <summary>
+ /// Converts the secret recovery task to
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>A task whos result the base64 decoded secret as a byte[]</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="InternalBufferTooSmallException"></exception>
+ public static async Task<byte[]?> ToBase64Bytes(this Task<SecretResult?> secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ using SecretResult? sec = await secret.ConfigureAwait(false);
+ return sec?.GetFromBase64();
+ }
+
+ /// <summary>
+ /// Recovers a certificate from a PEM encoded secret
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The <see cref="X509Certificate2"/> parsed from the PEM encoded data</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static X509Certificate2 GetCertificate(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ return X509Certificate2.CreateFromPem(secret.Result);
+ }
+
+ /// <summary>
+ /// Gets the secret value as a secret result
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The document parsed from the secret value</returns>
+ public static JsonDocument GetJsonDocument(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ //Alloc buffer, utf8 so 1 byte per char
+ using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length);
+ //Get utf8 bytes
+ int count = Encoding.UTF8.GetBytes(secret.Result, buffer.Span);
+ //Reader and parse
+ Utf8JsonReader reader = new(buffer.Span[..count]);
+ return JsonDocument.ParseValue(ref reader);
+ }
+
+ /// <summary>
+ /// Gets a SPKI encoded public key from a secret
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The <see cref="PublicKey"/> parsed from the SPKI public key</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static PublicKey GetPublicKey(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ //Alloc buffer, base64 is larger than binary value so char len is large enough
+ using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length);
+ //Get base64 bytes
+ ERRNO count = VnEncoding.TryFromBase64Chars(secret.Result, buffer.Span);
+ //Parse the SPKI from base64
+ return PublicKey.CreateFromSubjectPublicKeyInfo(buffer.Span[..(int)count], out _);
+ }
+
+ /// <summary>
+ /// Gets the value of the <see cref="SecretResult"/> as a <see cref="PrivateKey"/>
+ /// container
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The <see cref="PrivateKey"/> from the secret value</returns>
+ /// <exception cref="FormatException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static PrivateKey GetPrivateKey(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ return new PrivateKey(secret);
+ }
+
+ /// <summary>
+ /// Gets a <see cref="ReadOnlyJsonWebKey"/> from a secret value
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the result</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static ReadOnlyJsonWebKey GetJsonWebKey(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ //Alloc buffer, utf8 so 1 byte per char
+ using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length);
+ //Get utf8 bytes
+ int count = Encoding.UTF8.GetBytes(secret.Result, buffer.Span);
+ return new ReadOnlyJsonWebKey(buffer.Span[..count]);
+ }
+
+ /// <summary>
+ /// Gets a task that resolves a <see cref="ReadOnlyJsonWebKey"/>
+ /// from a <see cref="SecretResult"/> task
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or null if the secret was not found</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static async Task<ReadOnlyJsonWebKey?> ToJsonWebKey(this Task<SecretResult?> secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ using SecretResult? sec = await secret.ConfigureAwait(false);
+ return sec?.GetJsonWebKey();
+ }
+
+ /// <summary>
+ /// Gets a task that resolves a <see cref="ReadOnlyJsonWebKey"/>
+ /// from a <see cref="SecretResult"/> task
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <param name="required">
+ /// A value that inidcates that a value is required from the result,
+ /// or a <see cref="KeyNotFoundException"/> is raised
+ /// </param>
+ /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or null if the secret was not found</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static async Task<ReadOnlyJsonWebKey> ToJsonWebKey(this Task<SecretResult?> secret, bool required)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ using SecretResult? sec = await secret.ConfigureAwait(false);
+ //If required is true and result is null, raise an exception
+ return required && sec == null ? throw new KeyNotFoundException("A required secret was missing") : (sec?.GetJsonWebKey()!);
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.VNCache/LICENSE.txt b/lib/VNLib.Plugins.Extensions.VNCache/LICENSE.txt
new file mode 100644
index 0000000..147bcd6
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.VNCache/LICENSE.txt
@@ -0,0 +1,195 @@
+Copyright (c) 2022 Vaughn Nugent
+
+Contact information
+ Name: Vaughn Nugent
+ Email: public[at]vaughnnugent[dot]com
+ Website: https://www.vaughnnugent.com
+
+The software in this repository is licensed under the GNU Affero GPL version 3.0 (or any later version).
+
+GNU AFFERO GENERAL PUBLIC LICENSE
+
+Version 3, 19 November 2007
+
+Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+Preamble
+
+The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
+
+The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
+
+An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
+
+The precise terms and conditions for copying, distribution and modification follow.
+TERMS AND CONDITIONS
+0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the Program.
+
+To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
+1. Source Code.
+
+The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+2. Basic Permissions.
+
+All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
+4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
+5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
+ b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
+ c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
+ d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
+6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
+ b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
+ c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
+ d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
+ e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
+7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
+ b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
+ c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
+ d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
+ e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
+ f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
+12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
+13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
+
+Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.VNCache/README.md b/lib/VNLib.Plugins.Extensions.VNCache/README.md
new file mode 100644
index 0000000..dc387cd
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.VNCache/README.md
@@ -0,0 +1,5 @@
+# VNLib.Plugins.Extensions.VNCache
+*An extension library for configuring and managing a IGlobalCache connection against a VNCache cluster on a plugin*
+
+#### Builds
+Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my [website](https://www.vaughnnugent.com/resources/software). All tar-gzip (.tgz) files will have an associated .sha384 appended checksum of the desired download file. \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/VNCacheExtensions.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/VNCacheExtensions.cs
new file mode 100644
index 0000000..d4aa88a
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.VNCache/src/VNCacheExtensions.cs
@@ -0,0 +1,108 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.VNCache
+* File: VNCacheExtensions.cs
+*
+* VNCacheExtensions.cs is part of VNLib.Plugins.Extensions.VNCache which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json;
+
+using VNLib.Utils.Logging;
+using VNLib.Data.Caching;
+using VNLib.Data.Caching.Extensions;
+using VNLib.Plugins.Extensions.Loading;
+
+namespace VNLib.Plugins.Extensions.VNCache
+{
+ /// <summary>
+ /// Contains extension methods for aquiring a Plugin managed
+ /// global cache provider.
+ /// </summary>
+ public static class VNCacheExtensions
+ {
+ /// <summary>
+ /// Loads the shared cache provider for the current plugin
+ /// </summary>
+ /// <param name="pbase"></param>
+ /// <param name="localized">A localized log provider to write cache logging information to</param>
+ /// <returns>The shared <see cref="IGlobalCacheProvider"/> </returns>
+ /// <remarks>
+ /// The returned instance, background work, logging, and its lifetime
+ /// are managed by the current plugin. Beware when calling this method
+ /// network connections may be spawend and managed in the background by
+ /// this library.
+ /// </remarks>
+ public static VnCacheClient GetGlobalCache(this PluginBase pbase, ILogProvider? localized = null)
+ => LoadingExtensions.GetOrCreateSingleton<VnCacheClient>(pbase, localized == null ? LoadCacheClient : (pbase) => LoadCacheClient(pbase, localized));
+
+ private static VnCacheClient LoadCacheClient(PluginBase pbase) => LoadCacheClient(pbase, pbase.Log);
+
+ private static VnCacheClient LoadCacheClient(PluginBase pbase, ILogProvider localized)
+ {
+ //Get config for client
+ IReadOnlyDictionary<string, JsonElement> config = pbase.GetConfigForType<VnCacheClient>();
+
+ //Init client
+ ILogProvider? debugLog = pbase.IsDebug() ? pbase.Log : null;
+ VnCacheClient client = new(debugLog);
+
+ //Begin cache connections by scheduling a task on the plugin's scheduler
+ _ = pbase.DeferTask(() => RunClientAsync(pbase, config, localized, client), 250);
+
+ return client;
+ }
+
+ private static async Task RunClientAsync(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config, ILogProvider localized, VnCacheClient client)
+ {
+ ILogProvider Log = localized;
+
+ try
+ {
+ //Try loading config
+ await client.LoadConfigAsync(pbase, config);
+
+ Log.Verbose("VNCache client configration loaded successfully");
+
+ //Run and wait for exit
+ await client.RunAsync(Log, pbase.UnloadToken);
+ }
+ catch (OperationCanceledException)
+ { }
+ catch (KeyNotFoundException e)
+ {
+ Log.Error("Missing required configuration variable for VnCache client: {0}", e.Message);
+ }
+ catch (FBMServerNegiationException fne)
+ {
+ Log.Error("Failed to negotiate connection with cache server {reason}", fne.Message);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Cache client error occured in session provider");
+ }
+ finally
+ {
+ client.Dispose();
+ }
+
+ Log.Information("Cache client exited");
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/VNLib.Plugins.Extensions.VNCache.csproj b/lib/VNLib.Plugins.Extensions.VNCache/src/VNLib.Plugins.Extensions.VNCache.csproj
new file mode 100644
index 0000000..f913366
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.VNCache/src/VNLib.Plugins.Extensions.VNCache.csproj
@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <Version>1.0.1.1</Version>
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\VNLib\Hashing\src\VNLib.Hashing.Portable.csproj" />
+ <ProjectReference Include="..\..\..\VNLib\VNLib.Net.Messaging.FBM\src\VNLib.Net.Messaging.FBM.csproj" />
+ <ProjectReference Include="..\..\DataCaching\VNLib.Data.Caching.Extensions\VNLib.Data.Caching.Extensions.csproj" />
+ <ProjectReference Include="..\..\DataCaching\VNLib.Data.Caching\VNLib.Data.Caching.csproj" />
+ <ProjectReference Include="..\..\PluginBase\VNLib.Plugins.PluginBase.csproj" />
+ <ProjectReference Include="..\VNLib.Plugins.Extensions.Loading\VNLib.Plugins.Extensions.Loading.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/VnCacheClient.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/VnCacheClient.cs
new file mode 100644
index 0000000..a34e611
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.VNCache/src/VnCacheClient.cs
@@ -0,0 +1,231 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.VNCache
+* File: VnCacheClient.cs
+*
+* VnCacheClient.cs is part of VNLib.Plugins.Extensions.VNCache which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json;
+using System.Net.Sockets;
+using System.Net.WebSockets;
+using System.Security.Cryptography;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Hashing.IdentityUtility;
+using VNLib.Data.Caching;
+using VNLib.Data.Caching.Extensions;
+using VNLib.Net.Messaging.FBM.Client;
+using VNLib.Plugins.Extensions.Loading;
+
+
+namespace VNLib.Plugins.Extensions.VNCache
+{
+ /// <summary>
+ /// A wrapper to simplify a shared global cache client
+ /// </summary>
+ [ConfigurationName("vncache")]
+ public sealed class VnCacheClient : VnDisposeable, IGlobalCacheProvider
+ {
+ FBMClient? _client;
+
+ private TimeSpan RetryInterval;
+
+ private readonly ILogProvider? DebugLog;
+ private readonly IUnmangedHeap? ClientHeap;
+
+ /// <summary>
+ /// Initializes an emtpy client wrapper that still requires
+ /// configuration loading
+ /// </summary>
+ /// <param name="debugLog">An optional debugging log</param>
+ /// <param name="heap">An optional <see cref="IUnmangedHeap"/> for <see cref="FBMClient"/> buffers</param>
+ internal VnCacheClient(ILogProvider? debugLog, IUnmangedHeap? heap = null)
+ {
+ DebugLog = debugLog;
+ //Default to 10 seconds
+ RetryInterval = TimeSpan.FromSeconds(10);
+
+ ClientHeap = heap;
+ }
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ _client?.Dispose();
+ _client = null;
+ }
+
+
+ /// <summary>
+ /// Loads required configuration variables from the config store and
+ /// intializes the interal client
+ /// </summary>
+ /// <param name="pbase"></param>
+ /// <param name="config">A dictionary of configuration varables</param>
+ /// <exception cref="KeyNotFoundException"></exception>
+ internal async Task LoadConfigAsync(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config)
+ {
+ int maxMessageSize = config["max_message_size"].GetInt32();
+ string? brokerAddress = config["broker_address"].GetString() ?? throw new KeyNotFoundException("Missing required configuration variable broker_address");
+
+ //Get keys async
+ Task<ReadOnlyJsonWebKey?> clientPrivTask = pbase.TryGetSecretAsync("client_private_key").ToJsonWebKey();
+ Task<ReadOnlyJsonWebKey?> brokerPubTask = pbase.TryGetSecretAsync("broker_public_key").ToJsonWebKey();
+ Task<ReadOnlyJsonWebKey?> cachePubTask = pbase.TryGetSecretAsync("cache_public_key").ToJsonWebKey();
+
+ //Wait for all tasks to complete
+ _ = await Task.WhenAll(clientPrivTask, brokerPubTask, cachePubTask);
+
+ ReadOnlyJsonWebKey clientPriv = await clientPrivTask ?? throw new KeyNotFoundException("Missing required secret client_private_key");
+ ReadOnlyJsonWebKey brokerPub = await brokerPubTask ?? throw new KeyNotFoundException("Missing required secret broker_public_key");
+ ReadOnlyJsonWebKey cachePub = await cachePubTask ?? throw new KeyNotFoundException("Missing required secret cache_public_key");
+
+ RetryInterval = config["retry_interval_sec"].GetTimeSpan(TimeParseType.Seconds);
+
+ Uri brokerUri = new(brokerAddress);
+
+ //Init the client with default settings
+ FBMClientConfig conf = FBMDataCacheExtensions.GetDefaultConfig(ClientHeap ?? Memory.Shared, maxMessageSize, DebugLog);
+
+ _client = new(conf);
+
+ //Add the configuration to the client
+ _client.GetCacheConfiguration()
+ .WithBroker(brokerUri)
+ .WithVerificationKey(cachePub)
+ .WithSigningCertificate(clientPriv)
+ .WithBrokerVerificationKey(brokerPub)
+ .WithTls(brokerUri.Scheme == Uri.UriSchemeHttps);
+ }
+
+ /// <summary>
+ /// Discovers nodes in the configured cluster and connects to a random node
+ /// </summary>
+ /// <param name="Log">A <see cref="ILogProvider"/> to write log events to</param>
+ /// <param name="cancellationToken">A token to cancel the operation</param>
+ /// <returns>A task that completes when the operation has been cancelled or an unrecoverable error occured</returns>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="OperationCanceledException"></exception>
+ internal async Task RunAsync(ILogProvider Log, CancellationToken cancellationToken)
+ {
+ _ = _client ?? throw new InvalidOperationException("Client configuration not loaded, cannot connect to cache servers");
+
+ while (true)
+ {
+ //Load the server list
+ ActiveServer[]? servers;
+ while (true)
+ {
+ try
+ {
+ Log.Debug("Discovering cluster nodes in broker");
+ //Get server list
+ servers = await _client.DiscoverCacheNodesAsync(cancellationToken);
+ break;
+ }
+ catch (HttpRequestException re) when (re.InnerException is SocketException)
+ {
+ Log.Warn("Broker server is unreachable");
+ }
+ catch (Exception ex)
+ {
+ Log.Warn("Failed to get server list from broker, reason {r}", ex.Message);
+ }
+
+ //Gen random ms delay
+ int randomMsDelay = RandomNumberGenerator.GetInt32(1000, 2000);
+ await Task.Delay(randomMsDelay, cancellationToken);
+ }
+
+ if (servers?.Length == 0)
+ {
+ Log.Warn("No cluster nodes found, retrying");
+ await Task.Delay(RetryInterval, cancellationToken);
+ continue;
+ }
+
+ try
+ {
+ Log.Debug("Connecting to random cache server");
+
+ //Connect to a random server
+ ActiveServer selected = await _client.ConnectToRandomCacheAsync(cancellationToken);
+ Log.Debug("Connected to cache server {s}", selected.ServerId);
+
+ //Set connection status flag
+ IsConnected = true;
+
+ //Wait for disconnect
+ await _client.WaitForExitAsync(cancellationToken);
+
+ Log.Debug("Cache server disconnected");
+ }
+ catch (WebSocketException wse)
+ {
+ Log.Warn("Failed to connect to cache server {reason}", wse.Message);
+ continue;
+ }
+ catch (HttpRequestException he) when (he.InnerException is SocketException)
+ {
+ Log.Debug("Failed to connect to random cache server server");
+ //Continue next loop
+ continue;
+ }
+ finally
+ {
+ IsConnected = false;
+ }
+ }
+ }
+
+
+ ///<inheritdoc/>
+ public bool IsConnected { get; private set; }
+
+
+ ///<inheritdoc/>
+ public Task AddOrUpdateAsync<T>(string key, string? newKey, T value, CancellationToken cancellation)
+ {
+ return !IsConnected
+ ? throw new InvalidOperationException("The underlying client is not connected to a cache node")
+ : _client!.AddOrUpdateObjectAsync(key, newKey, value, cancellation);
+ }
+
+ ///<inheritdoc/>
+ public Task DeleteAsync(string key, CancellationToken cancellation)
+ {
+ return !IsConnected
+ ? throw new InvalidOperationException("The underlying client is not connected to a cache node")
+ : _client!.DeleteObjectAsync(key, cancellation);
+ }
+
+
+ ///<inheritdoc/>
+ public Task<T?> GetAsync<T>(string key, CancellationToken cancellation)
+ {
+ return !IsConnected
+ ? throw new InvalidOperationException("The underlying client is not connected to a cache node")
+ : _client!.GetObjectAsync<T>(key, cancellation);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Validation/LICENSE.txt b/lib/VNLib.Plugins.Extensions.Validation/LICENSE.txt
new file mode 100644
index 0000000..147bcd6
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Validation/LICENSE.txt
@@ -0,0 +1,195 @@
+Copyright (c) 2022 Vaughn Nugent
+
+Contact information
+ Name: Vaughn Nugent
+ Email: public[at]vaughnnugent[dot]com
+ Website: https://www.vaughnnugent.com
+
+The software in this repository is licensed under the GNU Affero GPL version 3.0 (or any later version).
+
+GNU AFFERO GENERAL PUBLIC LICENSE
+
+Version 3, 19 November 2007
+
+Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+Preamble
+
+The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
+
+The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
+
+An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
+
+The precise terms and conditions for copying, distribution and modification follow.
+TERMS AND CONDITIONS
+0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the Program.
+
+To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
+1. Source Code.
+
+The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+2. Basic Permissions.
+
+All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
+4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
+5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
+ b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
+ c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
+ d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
+6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
+ b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
+ c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
+ d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
+ e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
+7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
+ b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
+ c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
+ d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
+ e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
+ f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
+12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
+13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
+
+Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Validation/README.md b/lib/VNLib.Plugins.Extensions.Validation/README.md
new file mode 100644
index 0000000..f0b4649
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Validation/README.md
@@ -0,0 +1,9 @@
+# VNLib.Plugins.Extensions.Validation
+*An extension library for data validation helpers and extension methods using FluentValidation*
+
+#### Builds
+Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my [website](https://www.vaughnnugent.com/resources/software). All tar-gzip (.tgz) files will have an associated .sha384 appended checksum of the desired download file.
+
+## FluentValidation
+
+This library includes a package reference to [FluentValidation](https://github.com/FluentValidation/FluentValidation) which can also be found on nuget.org. \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj b/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj
new file mode 100644
index 0000000..ef10ecf
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj
@@ -0,0 +1,36 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <Version>1.0.1.1</Version>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="FluentValidation" Version="11.3.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\VNLib\Plugins\src\VNLib.Plugins.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/lib/VNLib.Plugins.Extensions.Validation/src/ValErrWebMessage.cs b/lib/VNLib.Plugins.Extensions.Validation/src/ValErrWebMessage.cs
new file mode 100644
index 0000000..8e439da
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Validation/src/ValErrWebMessage.cs
@@ -0,0 +1,41 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Validation
+* File: ValErrWebMessage.cs
+*
+* ValErrWebMessage.cs is part of VNLib.Plugins.Extensions.Validation which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Validation is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Validation is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Validation. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Collections;
+using System.Text.Json.Serialization;
+
+namespace VNLib.Plugins.Extensions.Validation
+{
+ /// <summary>
+ /// Extends the <see cref="WebMessage"/> class with provisions for a collection of validations
+ /// </summary>
+ public class ValErrWebMessage : WebMessage
+ {
+ /// <summary>
+ /// A collection of error messages to send to clients
+ /// </summary>
+ [JsonPropertyName("errors")]
+ public ICollection Errors { get; set; }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Validation/src/ValidationErrorMessage.cs b/lib/VNLib.Plugins.Extensions.Validation/src/ValidationErrorMessage.cs
new file mode 100644
index 0000000..63a1d63
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Validation/src/ValidationErrorMessage.cs
@@ -0,0 +1,36 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Validation
+* File: ValidationErrorMessage.cs
+*
+* ValidationErrorMessage.cs is part of VNLib.Plugins.Extensions.Validation which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Validation is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Validation is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Validation. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json.Serialization;
+
+namespace VNLib.Plugins.Extensions.Validation
+{
+ public class ValidationErrorMessage
+ {
+ [JsonPropertyName("property")]
+ public string PropertyName { get; set; }
+ [JsonPropertyName("message")]
+ public string ErrorMessage { get; set; }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Validation/src/ValidationExtensions.cs b/lib/VNLib.Plugins.Extensions.Validation/src/ValidationExtensions.cs
new file mode 100644
index 0000000..7ca45e9
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Validation/src/ValidationExtensions.cs
@@ -0,0 +1,78 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Validation
+* File: ValidationExtensions.cs
+*
+* ValidationExtensions.cs is part of VNLib.Plugins.Extensions.Validation which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Validation is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Validation is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Validation. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+using FluentValidation;
+using FluentValidation.Results;
+
+#nullable enable
+
+namespace VNLib.Plugins.Extensions.Validation
+{
+ /// <summary>
+ /// Provides shortcut methods to aid programmatic validation of objects
+ /// </summary>
+ public static class ValidationExtensions
+ {
+ /// <summary>
+ /// If <paramref name="assertion"/> evalues to false, sets the specified assertion message
+ /// to the <see cref="WebMessage.Result"/> to the specified string
+ /// </summary>
+ /// <param name="webm"></param>
+ /// <param name="assertion">The result of the assertion</param>
+ /// <param name="message">The error message to store when the value is false</param>
+ /// <returns>The inverse of <paramref name="assertion"/></returns>
+ public static bool Assert(this WebMessage webm, [DoesNotReturnIf(false)] bool assertion, string message)
+ {
+ if(!assertion)
+ {
+ webm.Success = false;
+ webm.Result = message;
+ }
+ return !assertion;
+ }
+ /// <summary>
+ /// Validates the specified instance, and stores errors to the specified <paramref name="webMessage"/>
+ /// and sets the <see cref="ValErrWebMessage.IsError"/>
+ /// </summary>
+ /// <param name="instance">The instance to validate</param>
+ /// <param name="validator"></param>
+ /// <param name="webMessage">The <see cref="ValErrWebMessage"/> to store errors to</param>
+ /// <returns>True if the result of the validation is valid, false otherwise and the <paramref name="webMessage"/> is not modified</returns>
+ public static bool Validate<T>(this IValidator<T> validator, T instance, ValErrWebMessage webMessage)
+ {
+ //Validate value
+ ValidationResult result = validator.Validate(instance);
+ //If not valid, set errors on web message
+ if (!result.IsValid)
+ {
+ webMessage.Success = false;
+ webMessage.Errors = result.GetErrorsAsCollection();
+ }
+ return result.IsValid;
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Validation/src/ValidatorExtensions.cs b/lib/VNLib.Plugins.Extensions.Validation/src/ValidatorExtensions.cs
new file mode 100644
index 0000000..63b4f07
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Validation/src/ValidatorExtensions.cs
@@ -0,0 +1,205 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Validation
+* File: ValidatorExtensions.cs
+*
+* ValidatorExtensions.cs is part of VNLib.Plugins.Extensions.Validation which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Validation is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Validation is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Validation. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Collections;
+using System.Text.RegularExpressions;
+
+using FluentValidation;
+using FluentValidation.Results;
+
+namespace VNLib.Plugins.Extensions.Validation
+{
+ /// <summary>
+ /// Defines extenstion methods for <see cref="IRuleBuilder{T, TProperty}"/>
+ /// </summary>
+ public static class ValidatorExtensions
+ {
+ public static readonly Regex PhoneRegex = new(@"^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$", RegexOptions.Compiled);
+
+ public static readonly Regex AlphaRegx = new(@"[a-zA-Z]*", RegexOptions.Compiled);
+ public static readonly Regex NumericRegx = new(@"[0-9]*", RegexOptions.Compiled);
+ public static readonly Regex AlphaNumRegx = new(@"[a-zA-Z0-9]*", RegexOptions.Compiled);
+
+ public static readonly Regex OnlyAlphaRegx = new(@"^[a-zA-Z\s]*$", RegexOptions.Compiled);
+ public static readonly Regex OnlyNumericRegx = new(@"^[0-9]*$", RegexOptions.Compiled);
+ public static readonly Regex OnlyAlphaNumRegx = new(@"^[a-zA-Z0-9\s]*$", RegexOptions.Compiled);
+
+ public static readonly Regex PasswordRegx = new(@"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-])", RegexOptions.Compiled);
+ public static readonly Regex IllegalRegx = new(@"[\r\n\t\a\b\e\f|^~`<>{}]", RegexOptions.Compiled);
+ public static readonly Regex SpecialCharactersRegx = new(@"[\r\n\t\a\b\e\f#?!@$%^&*\+\-\~`|<>\{}]", RegexOptions.Compiled);
+
+
+ /// <summary>
+ /// Gets a collection of Json-serializable validation errors
+ /// </summary>
+ /// <param name="result"></param>
+ /// <returns>A collection of json errors to return to a user</returns>
+ public static ICollection GetErrorsAsCollection(this ValidationResult result)
+ {
+ return result.Errors.ConvertAll(static err => new ValidationErrorMessage { ErrorMessage = err.ErrorMessage, PropertyName = err.PropertyName });
+ }
+
+ /// <summary>
+ /// Tests the the property against <see cref="PhoneRegex"/>
+ /// to determine if the string matches the proper phone number form
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> PhoneNumber<T>(this IRuleBuilder<T, string> builder)
+ {
+ return builder.Must(static phone => phone?.Length > 0 && PhoneRegex.IsMatch(phone))
+ .WithMessage("{PropertyValue} is not a valid phone number.");
+ }
+ /// <summary>
+ /// Tests the the property against <see cref="PhoneRegex"/>
+ /// to determine if the string matches the proper phone number form, or allows emtpy strings
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> EmptyPhoneNumber<T>(this IRuleBuilder<T, string> builder)
+ {
+ return builder.Must(static phone => phone == null || phone.Length == 0 || PhoneRegex.IsMatch(phone))
+ .WithMessage("{PropertyValue} is not a valid phone number.");
+ }
+
+ /// <summary>
+ /// Checks a string against <see cref="SpecialCharactersRegx"/>.
+ /// If the string is null or empty, it is allowed.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> SpecialCharacters<T>(this IRuleBuilder<T, string> builder)
+ {
+ return builder.Must(static str => str == null || !SpecialCharactersRegx.IsMatch(str))
+ .WithMessage("{PropertyName} contains illegal characters");
+ }
+ /// <summary>
+ /// Checks a string against <see cref="Statics.IllegalChars"/>.
+ /// If the string is null or empty, it is allowed.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> IllegalCharacters<T>(this IRuleBuilder<T, string> builder)
+ {
+ return builder.Must(static str => str == null || !IllegalRegx.IsMatch(str))
+ .WithMessage("{PropertyName} contains illegal characters");
+ }
+ /// <summary>
+ /// Makes sure a field contains at least 1 character a-Z
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> Alpha<T>(this IRuleBuilder<T, string> builder)
+ {
+ return builder.Must(static str => str == null || AlphaRegx.IsMatch(str))
+ .WithMessage("{PropertyName} requires at least one a-Z character.");
+ }
+ /// <summary>
+ /// Determines if all characters are only a-Z (allows whitespace)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> AlphaOnly<T>(this IRuleBuilder<T, string> builder)
+ {
+ return builder.Must(static str => str == null || OnlyAlphaRegx.IsMatch(str))
+ .WithMessage("{PropertyName} can only be a alpha character from a-Z.");
+ }
+ /// <summary>
+ /// Makes sure a field contains at least 1 numeral
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> Numeric<T>(this IRuleBuilder<T, string> builder)
+ {
+ return builder.Must(static str => str == null || NumericRegx.IsMatch(str))
+ .WithMessage("{PropertyName} requires at least one number.");
+ }
+ /// <summary>
+ /// Determines if all characters are only 0-9 (not whitespace is allowed)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> NumericOnly<T>(this IRuleBuilder<T, string> builder)
+ {
+ return builder.Must(static str => str == null || OnlyNumericRegx.IsMatch(str))
+ .WithMessage("{PropertyName} can only be a number 0-9.");
+ }
+ /// <summary>
+ /// Makes sure the field contains at least 1 alpha numeric character (whitespace included)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> AlphaNumeric<T>(this IRuleBuilder<T, string> builder)
+ {
+ return builder.Must(static str => str == null || AlphaNumRegx.IsMatch(str))
+ .WithMessage("{PropertyName} must contain at least one alpha-numeric character.");
+ }
+ /// <summary>
+ /// Determines if all characters are only alpha-numeric (whitespace allowed)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> AlphaNumericOnly<T>(this IRuleBuilder<T, string> builder)
+ {
+ return builder.Must(static str => str == null || OnlyAlphaNumRegx.IsMatch(str))
+ .WithMessage("{PropertyName} can only contain alpha numeric characters.");
+ }
+ /// <summary>
+ /// Tests the string against the password regular expression to determine if the
+ /// value meets the basic password requirements
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> Password<T>(this IRuleBuilder<T, string> builder)
+ {
+ return builder.Must(static str => str == null || PasswordRegx.IsMatch(str))
+ .WithMessage("{PropertyName} does not meet password requirements.");
+ }
+ /// <summary>
+ /// Defines a length validator on the current rule builder, but only for string properties.
+ /// Validation will fail if the length of the string is outside of the specified range.
+ /// The range is inclusive
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="builder"></param>
+ /// <param name="lengthRange">The length range of the specified string</param>
+ /// <returns></returns>
+ public static IRuleBuilderOptions<T, string> Length<T>(this IRuleBuilder<T, string> builder, Range lengthRange)
+ {
+ return builder.Length(lengthRange.Start.Value, lengthRange.End.Value);
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extentions.TransactionalEmail/LICENSE.txt b/lib/VNLib.Plugins.Extentions.TransactionalEmail/LICENSE.txt
new file mode 100644
index 0000000..147bcd6
--- /dev/null
+++ b/lib/VNLib.Plugins.Extentions.TransactionalEmail/LICENSE.txt
@@ -0,0 +1,195 @@
+Copyright (c) 2022 Vaughn Nugent
+
+Contact information
+ Name: Vaughn Nugent
+ Email: public[at]vaughnnugent[dot]com
+ Website: https://www.vaughnnugent.com
+
+The software in this repository is licensed under the GNU Affero GPL version 3.0 (or any later version).
+
+GNU AFFERO GENERAL PUBLIC LICENSE
+
+Version 3, 19 November 2007
+
+Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+Preamble
+
+The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
+
+The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
+
+An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
+
+The precise terms and conditions for copying, distribution and modification follow.
+TERMS AND CONDITIONS
+0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the Program.
+
+To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
+1. Source Code.
+
+The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+2. Basic Permissions.
+
+All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
+4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
+5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
+ b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
+ c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
+ d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
+6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
+ b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
+ c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
+ d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
+ e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
+7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
+ b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
+ c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
+ d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
+ e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
+ f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
+12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
+13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
+
+Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extentions.TransactionalEmail/README.md b/lib/VNLib.Plugins.Extentions.TransactionalEmail/README.md
new file mode 100644
index 0000000..6598fde
--- /dev/null
+++ b/lib/VNLib.Plugins.Extentions.TransactionalEmail/README.md
@@ -0,0 +1,5 @@
+# VNLib.Plugins.Extensions.TransactionalEmail
+*An extension library for building transactional email configurations to send programmable emails against a transactional email server*
+
+#### Builds
+Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my [website](https://www.vaughnnugent.com/resources/software). All tar-gzip (.tgz) files will have an associated .sha384 appended checksum of the desired download file. \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extentions.TransactionalEmail/src/EmailSystemConfig.cs b/lib/VNLib.Plugins.Extentions.TransactionalEmail/src/EmailSystemConfig.cs
new file mode 100644
index 0000000..5d7406a
--- /dev/null
+++ b/lib/VNLib.Plugins.Extentions.TransactionalEmail/src/EmailSystemConfig.cs
@@ -0,0 +1,54 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extentions.TransactionalEmail
+* File: EmailSystemConfig.cs
+*
+* EmailSystemConfig.cs is part of VNLib.Plugins.Extentions.TransactionalEmail which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extentions.TransactionalEmail is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extentions.TransactionalEmail is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extentions.TransactionalEmail. If not, see http://www.gnu.org/licenses/.
+*/
+
+using RestSharp;
+
+using Emails.Transactional.Client;
+using VNLib.Net.Rest.Client;
+
+
+namespace VNLib.Plugins.Extentions.TransactionalEmail
+{
+ /// <summary>
+ /// An extended <see cref="TransactionalEmailConfig"/> configuration
+ /// object that contains a <see cref="Net.Rest.Client.RestClientPool"/> pool for making
+ /// transactions
+ /// </summary>
+ internal sealed class EmailSystemConfig : TransactionalEmailConfig
+ {
+ /// <summary>
+ /// A shared <see cref="Net.Rest.Client.RestClientPool"/> for renting configuraed
+ /// <see cref="RestClient"/>
+ /// </summary>
+ public RestClientPool RestClientPool { get; init; }
+ /// <summary>
+ /// A global from email address name
+ /// </summary>
+ public string EmailFromName { get; init; }
+ /// <summary>
+ /// A global from email address
+ /// </summary>
+ public string EmailFromAddress { get; init; }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extentions.TransactionalEmail/src/TransactionalEmailExtensions.cs b/lib/VNLib.Plugins.Extentions.TransactionalEmail/src/TransactionalEmailExtensions.cs
new file mode 100644
index 0000000..4ad0fa9
--- /dev/null
+++ b/lib/VNLib.Plugins.Extentions.TransactionalEmail/src/TransactionalEmailExtensions.cs
@@ -0,0 +1,161 @@
+using System.Text;
+using System.Text.Json;
+
+using RestSharp;
+
+using Emails.Transactional.Client;
+
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Net.Rest.Client;
+using VNLib.Net.Rest.Client.OAuth2;
+using VNLib.Plugins.Extensions.Loading;
+
+
+namespace VNLib.Plugins.Extentions.TransactionalEmail
+{
+ /// <summary>
+ /// Contains extension methods for implementing templated
+ /// transactional emails
+ /// </summary>
+ public static class TransactionalEmailExtensions
+ {
+ public const string EMAIL_CONFIG_KEY = "emails";
+ public const string REQUIRED_EMAIL_TEMPALTE_CONFIG_KEY = "required_email_templates";
+
+ public const uint DEFAULT_MAX_CLIENTS = 5;
+ public const uint DEFAULT_CLIENT_TIMEOUT_MS = 10000;
+
+ /// <summary>
+ /// Gets (or loads) the ambient <see cref="TransactionalEmailConfig"/> configuration object
+ /// to send transactional emails against
+ /// </summary>
+ /// <param name="pbase"></param>
+ /// <returns>The <see cref="TransactionalEmailConfig"/> from the current plugins config</returns>
+ public static TransactionalEmailConfig GetEmailConfig(this PluginBase pbase) => LoadingExtensions.GetOrCreateSingleton(pbase, LoadConfig);
+
+ /// <summary>
+ /// Sends an <see cref="EmailTransactionRequest"/> on the current configuration resource pool
+ /// </summary>
+ /// <param name="config"></param>
+ /// <param name="request">The <see cref="EmailTransactionRequest"/> request to send to the server</param>
+ /// <returns>A task the resolves the <see cref="TransactionResult"/> of the request</returns>
+ public static async Task<TransactionResult> SendEmailAsync(this TransactionalEmailConfig config, EmailTransactionRequest request)
+ {
+ //Get a new client contract from the configuration's pool assuming its a EmailSystemConfig class
+ using ClientContract client = ((EmailSystemConfig)config).RestClientPool.Lease();
+ //Send the email and await the result before releasing the client
+ return await client.Resource.SendEmailAsync(request);
+ }
+
+ private static TransactionalEmailConfig LoadConfig(PluginBase pbase)
+ {
+ //Get the required email config
+ IReadOnlyDictionary<string, JsonElement> conf = pbase.GetConfig(EMAIL_CONFIG_KEY);
+
+ string emailFromName = conf["from_name"].GetString() ?? throw new KeyNotFoundException("Missing required configuration key 'from_name'");
+ string emailFromAddress = conf["from_address"].GetString() ?? throw new KeyNotFoundException("Missing required configuration key 'from_address'");
+ Uri baseServerPath = new(conf["base_url"].GetString()!, UriKind.RelativeOrAbsolute);
+
+ //Get the token server url or use the base path if no set
+ Uri tokenServerBase = conf.TryGetValue("token_server_url", out JsonElement tksEl) && tksEl.GetString() != null ?
+ new(tksEl.GetString()!, UriKind.RelativeOrAbsolute)
+ : baseServerPath;
+
+ //Get the transaction endpoint path, should be a realative path
+ Uri transactionEndpoint = new(conf["transaction_path"].GetString()!, UriKind.Relative);
+
+ //Load credentials
+ string authEndpoint = conf["token_path"].GetString() ?? throw new KeyNotFoundException("Missing required configuration key 'token_path'");
+
+ //Optional user-agent
+ string? userAgent = conf.GetPropString("user_agent");
+
+ //Get optional timeout ms
+ int timeoutMs = (int)(conf.TryGetValue("request_timeout_ms", out JsonElement timeoutEl) ? timeoutEl.GetUInt32() : DEFAULT_CLIENT_TIMEOUT_MS);
+
+ //Get maximum client limit
+ int maxClients = (int)(conf.TryGetValue("max_clients", out JsonElement mxcEl) ? mxcEl.GetUInt32() : DEFAULT_MAX_CLIENTS);
+
+ //Load all templates from the plugin config
+ Dictionary<string, string> templates = pbase.PluginConfig.GetProperty(REQUIRED_EMAIL_TEMPALTE_CONFIG_KEY)
+ .EnumerateObject()
+ .ToDictionary(static jp => jp.Name, static jp => jp.Value.GetString()!);
+
+ pbase.Log.Verbose("Required email templates {t}", templates);
+
+ //Load oauth secrets from vault
+ Task<SecretResult?> oauth2ClientID = pbase.TryGetSecretAsync("email_client_id");
+ Task<SecretResult?> oauth2Password = pbase.TryGetSecretAsync("email_client_secret");
+
+ //Lazy cred loaded, tasks should be loaded before this method will ever get called
+ Credential lazyCredentialGet()
+ {
+ //Load the results
+ SecretResult cliendId = oauth2ClientID.GetAwaiter().GetResult() ?? throw new KeyNotFoundException("Missing required oauth2 client id");
+ SecretResult password = oauth2Password.GetAwaiter().GetResult() ?? throw new KeyNotFoundException("Missing required oauth2 client secret");
+
+ //Creat credential
+ return Credential.Create(cliendId.Result, password.Result);
+ }
+
+ //Init client creation options
+ RestClientOptions poolOptions = new(baseServerPath)
+ {
+ AllowMultipleDefaultParametersWithSameName = true,
+ AutomaticDecompression = System.Net.DecompressionMethods.All,
+ PreAuthenticate = true,
+ Encoding = Encoding.UTF8,
+ MaxTimeout = timeoutMs,
+ UserAgent = userAgent,
+ //Server should not redirect
+ FollowRedirects = false,
+ };
+
+ //Options for auth token endpoint
+ RestClientOptions oAuth2ClientOptions = new(tokenServerBase)
+ {
+ AllowMultipleDefaultParametersWithSameName = true,
+ //Server supports compression
+ AutomaticDecompression = System.Net.DecompressionMethods.All,
+ PreAuthenticate = false,
+ Encoding = Encoding.UTF8,
+ MaxTimeout = timeoutMs,
+ UserAgent = userAgent,
+ //Server should not redirect
+ FollowRedirects = false
+ };
+
+ //Init Oauth authenticator
+ OAuth2Authenticator authenticator = new(oAuth2ClientOptions, lazyCredentialGet, authEndpoint);
+
+ //Create client pool
+ RestClientPool pool = new(maxClients, poolOptions, authenticator: authenticator);
+
+ void Cleanup()
+ {
+ authenticator.Dispose();
+ pool.Dispose();
+ oauth2ClientID.Dispose();
+ oauth2Password.Dispose();
+ }
+
+ //register password cleanup
+ _ = pbase.RegisterForUnload(Cleanup);
+
+ //Create config
+ EmailSystemConfig config = new ()
+ {
+ EmailFromName = emailFromName,
+ EmailFromAddress = emailFromAddress,
+ RestClientPool = pool,
+ };
+
+ //Store templates and set service url
+ config.WithTemplates(templates)
+ .WithUrl(transactionEndpoint);
+
+ return config;
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extentions.TransactionalEmail/src/VNLib.Plugins.Extentions.TransactionalEmail.csproj b/lib/VNLib.Plugins.Extentions.TransactionalEmail/src/VNLib.Plugins.Extentions.TransactionalEmail.csproj
new file mode 100644
index 0000000..f15a3ba
--- /dev/null
+++ b/lib/VNLib.Plugins.Extentions.TransactionalEmail/src/VNLib.Plugins.Extentions.TransactionalEmail.csproj
@@ -0,0 +1,36 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="RestSharp" Version="108.0.3" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\VNLib\Hashing\src\VNLib.Hashing.Portable.csproj" />
+ <ProjectReference Include="..\..\..\VNLib\Utils\src\VNLib.Utils.csproj" />
+ <ProjectReference Include="..\..\Emails.Transactional\Emails.Transactional.Client\Emails.Transactional.Client.csproj" />
+ <ProjectReference Include="..\..\PluginBase\VNLib.Plugins.PluginBase.csproj" />
+ <ProjectReference Include="..\..\VNLib.Net.Rest.Client\src\VNLib.Net.Rest.Client.csproj" />
+ <ProjectReference Include="..\VNLib.Plugins.Extensions.Loading\VNLib.Plugins.Extensions.Loading.csproj" />
+ </ItemGroup>
+
+</Project>