aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Plugins.Extensions.Data
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-01-09 15:09:13 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-01-09 15:09:13 -0500
commita46c3bf452d287b50b2e7dd5a24f5995c9fd26f6 (patch)
tree3a978b2dd2887b5c0e25f595516594a647d8e880 /lib/VNLib.Plugins.Extensions.Data
parent189c6714057bf45553847eaeb9ce97eb7272eb8c (diff)
Restructure
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Data')
-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
32 files changed, 4127 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>