{"id":18276437,"url":"https://github.com/u-labs/ulabs.vbulletinentity","last_synced_at":"2026-05-05T21:33:33.760Z","repository":{"id":118313093,"uuid":"200629110","full_name":"U-Labs/ULabs.VBulletinEntity","owner":"U-Labs","description":"A vBulletin 4 database abstraction layer for Entity Framework Core and Dapper","archived":false,"fork":false,"pushed_at":"2022-12-08T14:52:24.000Z","size":929,"stargazers_count":1,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-04-01T23:51:31.493Z","etag":null,"topics":["c-sharp","csharp","dapper","entity-framework-core","mariadb","mysql","net-standard","vbulletin","vbulletin4"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/U-Labs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-08-05T09:54:54.000Z","updated_at":"2021-10-14T20:40:09.000Z","dependencies_parsed_at":null,"dependency_job_id":"d5d07631-07a7-4e93-b339-a1738357d2b3","html_url":"https://github.com/U-Labs/ULabs.VBulletinEntity","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/U-Labs%2FULabs.VBulletinEntity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/U-Labs%2FULabs.VBulletinEntity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/U-Labs%2FULabs.VBulletinEntity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/U-Labs%2FULabs.VBulletinEntity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/U-Labs","download_url":"https://codeload.github.com/U-Labs/ULabs.VBulletinEntity/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247974734,"owners_count":21026742,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c-sharp","csharp","dapper","entity-framework-core","mariadb","mysql","net-standard","vbulletin","vbulletin4"],"created_at":"2024-11-05T12:16:01.800Z","updated_at":"2026-05-05T21:33:28.734Z","avatar_url":"https://github.com/U-Labs.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"﻿# U-Labs VBulletin Entity\nA vBulletin 4 database abstraction layer for Entity Framework Core - OSS under [GNU GPLv3](https://choosealicense.com/licenses/gpl-3.0/). \nIt is based on the current V2.1 LTS release of Entity Framework Core.\n\n![Demo project preview](https://u-img.net/img/6729Br.png)\n\n## Feature state\n![Complete](https://u-img.net/img/8798Dw.png) Full completed \n![Partly implemented](https://u-img.net/img/5113Ab.png) Partly implemented\n![Not implemented yet](https://u-img.net/img/2301Ja.png) Not implemented (yet)\n\n### Entities\nEvery entity represents a database table. The following list shows which are already implemented/need some work. \n\n| Entity  | State | Comment |\n| ------------- | ------------- | -------------\n| VBSettings | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBThreadRead | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBThread | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBPost | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBPoll | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBForumPermission | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBForum | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBFileData | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBAttachment | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBMessage | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBMessageText | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBSession | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBCustomAvatar | ![Complete](https://u-img.net/img/8798Dw.png) |\n| VBUserGroup  | ![Partly implemented](https://u-img.net/img/5113Ab.png) | Forum \u0026 Admin permissions implemented, rest of the permissions missing\n| VBUser | ![Partly implemented](https://u-img.net/img/5113Ab.png) | Some enums need to figured out. A few values are unknown and implemented raw\n\n### Settings\nSettings are stored in a single table, while `VBSettingsManager` map them to entity models similar as the ones above. \n\n| Group model| State | Comment |\n| ------------- | ------------- | -------------\n| VBCommonSettings | ![Complete](https://u-img.net/img/8798Dw.png) | Designed as abstraction for setting groups\n\n## Usage\nInstall [the official NuGet-Package ULabs.VBulletinEntity](https://www.nuget.org/packages/ULabs.VBulletinEntity):\n```\n# Visual Studio Package Manager Console\nInstall-Package ULabs.VBulletinEntity\n# DotNet CLI (e.g. for VS Code)\ndotnet add package ULabs.VBulletinEntity\n```\n\n### Configure Connection Strings for Service Injection\nIn your `appsettings.json` insert a [Connection String](https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-strings) for the\nvBulletin MySQL database in the corresponding section like this:\n\n```json\n{\n  \"ConnectionStrings\": {\n    \"VBForum\": \"Server=localhost;Database=vb_forum;User=vb_forum;Password=xxx;\"\n  },\n  ...\n}\n```\n\nRegister the database services with the specified Connection String: \n\n```cs\nusing System;\nusing ULabs.VBulletinEntity.DatabaseExtensions;\nusing ULabs.VBulletinEntity.Caching;\nusing Pomelo.EntityFrameworkCore.MySql.Infrastructure;\n\nnamespace ULabs.VBulletinEntityDemo {\n    public class Startup {\n        public IConfiguration Configuration { get; }\n\n        public void ConfigureServices(IServiceCollection services) {\n            var vbConfig = new VBConfig(Configuration.GetValue\u003cstring\u003e(\"VBCookieSalt\"));\n            services.AddVBDbContext\u003cVBNoCacheDummy\u003e(vbConfig, Configuration.GetConnectionString(\"VBForum\"), new Version(10, 3, 17), ServerType.MariaDb);\n            // For using Managers, see above\n            services.AddVBManagers();\n            // ...\n        }\n    }\n}\n```\n\n`VBConfig` specifies configuration options which are only present in the VB filesystem like Cookie salt or cookie prefix (default `bb`). This is \ncurrently _only used for session handling_. \n\n#### Cookie salt\n\nYou'll find this as `COOKIE_SALT` constant in `{vBulletinInstallationDir}/includes/functions.php` at line 34:\n\n```php\ndefine('COOKIE_SALT', 'xyz');\n```\nIn this example, your salt is `xyz`. \n\n#### Cookie prefix\n\nRequired to fetch session cookies. Defined in `{vBulletinInstallationDir}/includes/config.php`:\n\n```php\n$config['Misc']['cookieprefix'] = 'bb';\n```\n\nIt's not required to pass the prefix if it wasn't manually changed. \n\n#### Tipp for local developing:\n\nUse `appsettings.Development.json` to override the Connection String. If `appsettings.Development.json` is added\nto your `.gitignore`, no credentials can be checked in by accident. This approach is used in our example project.  \nASP.NET Core automatically adds files following the pattern `appsettings.{Environment}.json` when existing. \nSo keep in mind, that this only works while `ASPNETCORE_ENVIRONMENT` is set to `Development`. \n\n### Caching\nPerformance can be improved by caching data received from the database. This results in a faster page load time and less server load. When using the managers,\nyou can enable in-memory caching by passing our cache-provider instead of the default dummy one: \n\n```cs\nservices.AddVBDbContext\u003cVBCache\u003e(vbConfig, Configuration.GetConnectionString(\"VBForum\"), new Version(10, 3, 17), ServerType.MariaDb);\n```\n\nThe `VBCache` provider simply uses \n[Microsoft.Extensions.Caching.Memory](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.1). So the data is \ncached in the memory of your application server. But you also could use e.g. Redis or any other caching system by implemeting a provider based \non [IVBCache interface](file://ULabs.VBulletinEntity/Cache/IVBCache.cs).\n\n#### Important\n\nThis cache is designed to work with our Manager abstractions. They care about invalidating the cache (e.g. post after modification). But it's not possible\nto automatically detect changes from outside, even not the DbContext. So **don't use** caching if you do write querys using raw DbContext, a vBulletin\ninstallation runs in parallel or any other software modifies the database from outside. \n\n## Data Access Layer\nYou can choose between three ways of accessing VB data: \n\n### [Low level DbContext](./ULabs.VBulletinEntity/VBDbContext.cs)\n\nWorking with a familar DbContext instance give maximum flexibility: Every query that EF can technically do is possible. But you have to care about \nlazy loading propertys by yourself. Same way for required fields (e.g. creation timestamps) on writing queries. Recommended for advanced users which \na deeper knowledge in vBulletins database, if the managers reach their limit for the use-case. \n\nIf you registered the service by calling `services.AddVBDbContext()` as described in the getting started guide, you're already done. \nSimply let .NET Core's DI inject the context to your controller like this:\n\n```cs\nnamespace ULabs.VBulletinEntityDemo.Controllers {\n    public class DbContextController : Controller {\n        readonly VBDbContext db;\n\n        public DbContextController(VBDbContext db) {\n            this.db = db;\n        }\n    }\n}\n```\n\nNow use the instance to write your LINQ queries. See the [NewestContentModel.cs](./ULabs.VBulletinEntityDemo/Models/NewestContentModel.cs) model\nin our example project. It shows how to fetch the newest users, threads, posts and active sessions. Also relations are included, for example\nthe forum of a thread. \n\n### High level Managers\n\nOur managers try to cover common use-cases for developing a .NET based board. This helps keeping VB and database related logic outside of your application\nproject. While the managers will extended if needed, they doesn't aim to cover every special use-case. \n\nManagers are registered by adding `services.AddVBManagers()` to our `Startup.ConfigureServices()` method. \nCurrently this public repo contains the following managers:\n\n* [`VBUserManager`](./ULabs.VBulletinEntity/Manager/VBUserManager.cs)\n* [`VBThreadManager`](./ULabs.VBulletinEntity/Manager/VBThreadManager.cs)\n* [`VBForumManager`](./ULabs.VBulletinEntity/Manager/VBForumManager.cs)\n* [`VBSettingsManager`](./ULabs.VBulletinEntity/Manager/VBSettingsManager.cs)\n* [`VBSessionManager`](./ULabs.VBulletinEntity/Manager/VBSessionManager.cs)\n* [`VBAttachmentManager`](./ULabs.VBulletinEntity/Manager/VBAttachmentManager.cs)\n\nSimply register the required service in e.g. a controller constructor. \n\n#### VBSettingsManager\nVBulletin has a lot of settings, divided into multiple groups. Addons can create their own settings groups. All of them got stored in the `setting` \ntable. To handle them in a clean and reuseable way, we use the `VBSettingsManager` to map every group to a entity model. The first one avaliable\nis `VBCommonSettings`. Find some working examples in the [SettingsController](./ULabs.VBulletinEntityDemo/Controllers/SettingsController.cs).\n\n#### VBSessionManager\nThe Session-Manager can fetch sessions created by VB to integrate your custom .NET Core applications. `VBSessionManager.GetCurrentAsync()`\nreturn a `VBSession` object with the related `VBUser` if he has an authenticated session. This _only works_ if the Kestrel Webserver from\nASP.NET Core has access to the cookies. So it's required that VBulletin and your .NET Core application were hosted on the same domain. If VB runs on a subdomain (forum.example.com), \nthe Cookie-Domain must be set to the TLD (example.com) in VBs Cookie settings. \n\nThis can be archived in the simplest way by running some XAMPP/LAMPP stack on `localhost` default configuration. When you sign in, cookies\nwere set for `localhost`. Now start the demo application and access it using http://localhost:5000 and SSO should work. \n\n#### VBThreadManager\nThe following examples assumes that a instance of `VBThreadManager` was injected as `threadManager`.\n\n##### Create thread\n```csharp\nstring userId = 1;\nint forumId = 1;\nstring title = \"Thread created by ULabs.VBulletinEntity\";\nstring text = \"This thread was automatically generated using .NET Core!\";\nstring ip = \"127.0.0.1\";\n\nvar user = await userManager.GetUserAsync(userId);\nvar thread = await threadManager.CreateThreadAsync(user, ip, forumId, title, text);\n```\n`CreateThreadAsync()` throws an exception if the forum passed as `forumId` parameter doesn't exist. All timestamps are set to the current\nUTC date. You can test this in the demo project with the following url: http://localhost:5000/thread/create?userId=1\u0026forumId=1\u0026title=TestApiThread\u0026text=SomeText\n\n##### Check permissions\nWe use `CheckAsync` suffixed methods to perform checks if a user is allowed to perform some action. Creating \na reply would be such an example, where you want to check permissions before creating posts:\n\n```csharp\nvar session = await sessionManager.GetCurrentAsync();\nvar replyModel = new CreateReplyModel(session.User, threadId: 1, text: \"Some reply content\", ipAddress: \"127.0.0.1\");\nvar replyCheck = await threadManager.CreateReplyCheckAsync(replyModel);\n\nif(replyCheck == CanReplyResult.Ok) {\n    var reply = await threadManager.CreateReplyAsync(replyModel);\n}else {\n    // Show some information about the error\n}\n```\n\n### [High level Light Managers](./docs/light-managers.md)\nOffering entities with common needed properties where you can work with using LINQ is fine. \nOn the other side, this costs performance. Depending on the use-case, it's not a problem if things take a few\nms more or less. But at U-Labs we have situations, where fast page loading time is required. To also cover this case,\nI introduced _Light Managers_: They follow the idea of relatively high managers, but without the cost of too much \nressources usage.\n\nFor this purpose, Light Managers doesn't query the database with LINQ to SQL. Instead, we use \n[Dapper](https://github.com/StackExchange/Dapper), a known high performance ORM for .NET and .NET Core. Use managers\nif query performance is important, but you also want to have the comfort of high level apis. But please check if \nthe Light Managers can fit our needs. They're designed for special use cases and can't be such flexible than\nthe regular managers. \n\nSee [the high level light managers documentation](./docs/light-managers.md) for more details. \n\n## Application Warmup\n[A _cold_ Database Context is much slower on the first usage than a _warm_ Context.](https://stackoverflow.com/questions/13250679/how-to-warm-up-entity-framework-when-does-it-get-cold). \nThis thread is a bit older, but the general problem also applys to EF Core as well as other ORMs: On the first request, everything needs to\nbe initialized from scatch. So the first request can be relatively slow, where all following requests will be much faster. Since this is not good for user experience, it's a good idea to warm up our Database Context. By doing this, the first user gets a faster\nexperience. There are to ways of doing this. \n\n### Database queries\n\nThis method is relatively simple: When starting our application, we create a `DbContext` instance and do a few requests on different \nentity types. In the main method of `Program.cs` replace `CreateWebHostBuilder(args).Build().Run();` by\n\n```cs\nusing ULabs.VBulletinEntity.Tools;\n\nnamespace ULabs.VBulletinEntityDemo {\n    public class Program {\n        public static void Main(string[] args) {\n            CreateWebHostBuilder(args).Build()\n                .WarmUp()\n                .Run();\n        }\n        // ...\n    }\n}\n```\nand see the requests in our Kestrel console. \n\n### HTTP Request\nMore effective is doing some real requests. In the best case, we call a MVC action with database queries because it would warm up the \ndatabase as well as the MVC framework. But it's a bit more work. First we create a controller (here called `WarmUp`) with corresponding \nview of the `Index` action:\n\n```cs\nusing ULabs.VBulletinEntity.Tools;\n\npublic class WarmUpController : Controller {\n    readonly VBDbContext db;\n    readonly VBThreadManager threadManager;\n    readonly VBSessionManager sessionManager;\n    readonly VBSettingsManager settingsManager;\n    readonly VBForumManager forumManager;\n    readonly VBUserManager userManager;\n\n    public WarmUpController(VBDbContext db, VBThreadManager threadManager, VBSessionManager sessionManager, VBSettingsManager settingsManager, VBForumManager forumManager, VBUserManager userManager) {\n        this.db = db;\n        this.threadManager = threadManager;\n        this.sessionManager = sessionManager;\n        this.settingsManager = settingsManager;\n        this.forumManager = forumManager;\n        this.userManager = userManager;\n    }\n\n    public ActionResult Index() {\n        DatabaseWarmUp.WarmUpServices(db, threadManager, sessionManager, settingsManager, forumManager, userManager);\n        return View();\n    }\n}\n```\n\nIt's not required to render those data in the corresponding view. Something small that is generated dynamically like this would be fine: \n\n```cs\n@DateTime.Now.ToString()\n```\n\nNow add a injection of `IApplicationLifetime` to `Startup.Configure()` and reqister the warmup request on the event that got fired\nwhen your aplication is started:\n\n```cs\nusing ULabs.VBulletinEntity.Tools;\n\nnamespace ULabs.VBulletinEntityDemo {\n    public class Startup {\n        // ...\n        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime appLife) {\n            appLife.ApplicationStarted.Register(() =\u003e DatabaseWarmUp.WarmUpRequest(\"WarmUp/Index\"));\n            // ...\n        }\n    }\n}\n```\n\n### Performance comparisation\n| Method | Page render time (ms)\n| ---  | --- \n| No warmup | 2119\n| Database queries on startup only | 800\n| HTTP Request only | 97\n| Database queries on startup + HTTP Request | 88\n\nAll tests run on Windows 10 x64 with the index page of our demo application, which contains multiple SQL queries with joins. \nAs you can see, the HTTP request with database queries is the most effective way which results in very fast pages. Combining it with the\ndatabase queries on startup, there is not a huge improvement.\n\n#### Why using both methods? \n\nThis is related to the application startup order: \n\n```cs\nCreateWebHostBuilder(args).Build()\n    .WarmUp()\n    .Run();\n```\n\nAs you can see, our warmup queries runs _before_ the WebServer got started. This will already pre-warmup the database part of our application.\nThe warm-up request to ``WarmUp/Index` is done _after_ this and would be finished faster. In a modern Kubernetes deployment, this affects\nthe pod readiness. In other words: Durin an update, we could keep the first page load as fast as possible by warming up as much as possible\nuntil Kubernetes consider the pod as ready and route traffic to it. \n\n## Addon support\nSome Addons apply modifications on the database like for example the [post thanks addon](https://www.vbulletin.org/forum/showthread.php?t=231666). \nCommonly, the core-tables were extended by custom columns. My idea was to seperate this by inheritance. In this case, we had a `VBUser` entity \nthat only contains core-attributes from VB itself. For the Addon we create a inherited entity that adds the new addon fields: \n\n```cs\nnamespace ULabs.VBulletinEntity.Models.AddOns {\n    [Table(\"user\")]\n    public class VBPostThanksUser : VBUser {\n        [Column(\"post_thanks_user_amount\")]\n        public int PostThanksCount { get; set; }\n\n        [Column(\"post_thanks_thanked_posts\")]\n        public int ThankedPostsCount { get; set; }\n        // ...\n    }\n}\n```\nSadly this isn't possible yet since [EF Core forces _discriminator_ columns](https://stackoverflow.com/questions/52588922/force-inherited-classes-in-asp-net-core-entity-framework-core-to-dedicated-mysql).\nIn case of inheritance, EF Core creates a column called _discriminator_ that contains the entity type (`VBUser` or `VBPostThanksUser` in this case). \nThis makes things complicated. \n\n[Until EF Core introduced _Table-per-Concret Type_ as a fix](https://github.com/aspnet/EntityFrameworkCore/issues/3170), I decided not to spend more time\non this . Instead, Addon support is keept at a absolute minimum. For us this is only the \n[common post thanks addon](https://www.vbulletin.org/forum/showthread.php?t=231666), which is heavily used on U-Labs since years. \nI'm open for ideas how we could seperate this better in the future. \n\n## Motivation\nThis project is part of my approach to develop on a modern .NET Core application stack for vBulletin. I did some POCs, also on the database.\nBut now it's time to create a better structure. Since I believe in open source and also use a lot of OSS, I'd also like to share my work to\ngive something back for the community.\n\n## Contributions/Coding Conventions\n\nPlease see our [dedicated conventions documentation](./docs/conventions.md) related to C# coding style and also vBulletin. \n\n## Credits\nThis project itself uses the following external open source libraries to which I would like to express my gratitude:\n* [Pomelo.EntityFrameworkCore.MySql](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql)\n* [Dapper](https://github.com/StackExchange/Dapper)\n* Icons from made by [Flaticons](https://www.flaticon.com/) by [Freepik](https://www.flaticon.com/authors/freepik),\n[Maxim Basinski](https://www.flaticon.com/authors/maxim-basinski) \nlicensed with [CCC 3.0 BY](http://creativecommons.org/licenses/by/3.0/)\n\n## Disclaimer\nI'm not associated with _MH Sub I, LLC dba vBulletin_, the company behind vBulletin community software. As a customer of vBulletin, \na valid licence for U-Labs is present. This is a private project which uses the term _vBulletin_ only to describe it's functionality. \nI do not make the brand vBulletin my own.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fu-labs%2Fulabs.vbulletinentity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fu-labs%2Fulabs.vbulletinentity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fu-labs%2Fulabs.vbulletinentity/lists"}