{"id":24253391,"url":"https://github.com/msz/trellis","last_synced_at":"2025-03-04T18:27:31.839Z","repository":{"id":80917671,"uuid":"42590018","full_name":"msz/trellis","owner":"msz","description":"🌳 C# lazy loading from data stores","archived":false,"fork":false,"pushed_at":"2015-09-18T21:26:10.000Z","size":160,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-15T03:58:19.289Z","etag":null,"topics":[],"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/msz.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}},"created_at":"2015-09-16T13:50:08.000Z","updated_at":"2021-04-08T00:22:21.000Z","dependencies_parsed_at":"2023-03-12T12:43:45.454Z","dependency_job_id":null,"html_url":"https://github.com/msz/trellis","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/msz%2Ftrellis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msz%2Ftrellis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msz%2Ftrellis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msz%2Ftrellis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/msz","download_url":"https://codeload.github.com/msz/trellis/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241898203,"owners_count":20039061,"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":[],"created_at":"2025-01-15T03:26:55.078Z","updated_at":"2025-03-04T18:27:31.812Z","avatar_url":"https://github.com/msz.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Introduction\nTrellis provides easy lazy loading capabilites for a database-like store. It is designed to work mainly with NoSQL databases. Assumptions about the database include:\n\n* It stores objects consisting of named fields with various values\n* It stores objects in named \"collections\"\n* It supports interacting with selected from an object and not others\n* It has support for array fields\n\nIf the database fulfills those requirements, an `IDB` interface implementation can be written for it and Trellis can be used.\n\n# LazyModels\nModels are the representation of an object in a collection in DB. \n\n## Configuration\n\n### The model class\nDatabase model classes need to inherit from `LazyModel`. Model fields are set up as properties. In the getter and setter methods, use `PropertyGetter()` and `PropertySetter()` inherited from `LazyModel`. Due to limitations of the language, this has to be done manually (may be later converted to PostSharp). The name of the field that you pass to property methods will be used as field name for the database adapter.\n\nThe model also needs a public constructor that will provide objects necessary for model creation.\n\nExample model:\n```csharp\npublic class UserAccountModel :LazyModel\n{\n    public string Username\n    {\n        get { return PropertyGetter(\"Username\"); }\n        set { PropertySetter(\"Username\", value); }\n    }\n    public DateTime CreatedAt\n    {\n        get { return PropertyGetter(\"CreatedAt\"); }\n        set { PropertySetter(\"CreatedAt\", value); }\n    }\n    \n    public UserAccountModel(Id id, IDBCollection collection)\n            :base(id, collection)\n    { }\n}\n```\n\n### The ModelProvider\nTrellis uses a `ModelProvider` class that builds on the `IDB` interface. It is used to query the database and retrieve appropriate models or to create new ones. Its constructor\n```\npublic ModelProvider(IDB db, IDictionary\u003cType, string\u003e collectionNameDict)\n```\nallows passing a dictionary defining collection names for different types of models. If there is no collection name defined for a model, a default, straightforward one will be used (\"UserModels\" for type `UserModel`, for example).\n\n## Usage\nHaving the configuration work done, using the models is super easy. Creation of `ModelProvider`s is best done with an IoC container. It is preferable to instatiate per-model generic versions of `ModelProvider` to maintain type safety.\n```csharp\nvar collectionNameConfig = new Dictionary\u003cType, string\u003e\n{\n    {typeof(UserAccountModel), \"Accounts\"}\n};\nvar provider = new ModelProvider(yourDatabaseAdapter, collectionNameConfig);\nvar accountProvider = new ModelProvider\u003cUserAccountModel\u003e(provider);\n```\n\n### Getting data from a model\n```csharp\nvar account = accountProvider.Get(0);\nvar username = account.Username;\n```\nThe `Get()` call itself does not perform any interaction with the database. Only when getting the Username field, an appropriate request will be sent to retrieve the value. This prevents loading unnecessary values from the model.\n\n#### Preload\nWhen you need to use a lot of fields from a model, the default behavior of one-dbcall-per-field becomes undesired. The `Preload()` method allows you to preload values in one batch dbcall before using them.\n```csharp\nvar account = accountProvider.Get(0);\n\n// DB call here\naccount.Preload(x =\u003e x.Username,\n                x =\u003e x.CreatedAt);\n                \n// No further DB calls\nvar username = account.Username;\nvar createdAt = account.CreatedAt;\n```\n\n### Setting model fields\n```csharp\nvar account = accountProvider.Get(0);\naccount.Username = \"banana\";\naccount.CreatedAt = new DateTime(2001, 1, 1);\n// DB call at this point\naccount.Commit();\n```\nSetting model fields does not interact with the DB. It instead journals your changes. Then you use the `Commit()` method to send one optimized DB write for the model.\n\n# Aggregators\nAggregators consist of several models and consolidate information from them into a single entity. They usually represent domain entities. \n\n## Configuration\n\n### The aggregator class\nThe aggregator class setup is similar to model setup. All aggregators inherit from LazyAggregator. One additional thing that you need to do is to setup a mapping from models to the aggregator. It is done through an AutoMapper-like fluent API. \n\nThe simplest use case is declaring only model types that the aggregator is using and Trellis will automatically map all properties with corresponding names and types from models to the aggregator. In more advanced cases, an explicit mapping definition is required.\n\nThe models that the aggregator is using all have to have the same ID. If an ID is different, it means the model requires a separate aggregator as it is a different entity.\n\nIn the constructor, you pass instances of the models that the aggregator is using.\n\nAssuming the following model definitions (getter and setter implementations omitted for clarity):\n```csharp\nclass PaymentDate : LazyModel\n{\n    public int DayPaid { ... }\n    public int MonthPaid { ... }\n    public int YearPaid { ... }\n    public PaymentDate(Id id, IDBCollection collection)\n        : base(id, collection)\n    {}\n}\n```\n```csharp\nclass PaymentRecord : LazyModel\n{\n    public int Amount { ... }\n    public string ProductName { ... }\n    public PaymentRecord(Id id, IDBCollection collection)\n        : base(id, collection)\n    {}\n}\n```\nwe can have the following aggregator setup:\n```csharp\nclass Payment : LazyAggregator\n{\n    public int PaymentAmount\n    {\n        get { return PropertyGetter\u003cint\u003e(\"PaymentAmount\"); }\n        set { PropertySetter(\"PaymentAmount\", value); }\n    }\n    public string ProductName\n    {\n        get { return PropertyGetter\u003cstring\u003e(\"ProductName\"); }\n        set { PropertySetter(\"ProductName\"), value); }\n    }\n    public DateTime Date\n    {\n        get { return PropertyGetter\u003cDateTime\u003e(\"Date\"); }\n        set { PropertySetter(\"Date\", value); }\n    }\n    public Payment(\n        IAggregatorProvider provider,\n        PaymentRecord record,\n        PaymentDate date)\n        : base(provider, record, date)\n    {}\n    static Payment()\n    {\n        Using\u003cPayment, PaymentRecord\u003e();\n        Using\u003cPayment, PaymentDate\u003e();\n        \n        Setup\u003cPayment\u003e()\n            .Field(x =\u003e x.PaymentAmount)\n                .OneToOne\u003cPaymentRecord\u003e(x =\u003e x.Amount)\n            .Field(x =\u003e x.Date)\n                .From(a =\u003e new DateTime(a.M\u003cPaymentDate\u003e().Year,\n                                        a.M\u003cPaymentDate\u003e().Month,\n                                        a.M\u003cPaymentDate\u003e().Day))\n                .To\u003cPaymentDate\u003e(x =\u003e x.DayPaid).With(dt =\u003e dt.Day)\n                .To\u003cPaymentDate\u003e(x =\u003e x.MonthPaid).With(dt =\u003e dt.Month)\n                .To\u003cPaymentDate\u003e(x =\u003e x.YearPaid).With(dt =\u003e dt.Year)\n                .Using\u003cPaymentDate\u003e(x =\u003e x.DayPaid,\n                                    x =\u003e x.MonthPaid,\n                                    x =\u003e x.YearPaid);\n    }\n}\n```\nIt's convenient to put the mapping configuration in the static constructor of the class, although it can be done somewhere else as long as it's before any aggregator operations. We will go through it step by step.\n\n#### *Using* declarations \nThey are in the form `LazyAggregator.Using\u003cAggregatorType, ModelType\u003e()` and declare that the aggregator of type `AggregatorType` is using model `ModelType`. They are needed for the automapping functionality to work. Model types in *Using* declarations should be consistent with aggregator constructor agrument types.\n\n#### Field mapping\n* Field `ProductName` has a corresponding field in the models with the same name and type. Therefore no explicit mapping is needed.\n* Field `PaymentAmount` maps to field `PaymentRecord.Amount` which is the same thing, but with a different name. We can setup a simple one-to-one mapping with `OneToOne\u003cModelType\u003e()`.\n* Field `Date` is more complicated because it uses several model fields to build a single aggregator value. We have to use a full explicit mapping configuration.\n    - `From()` is used to define a fuction that transforms model field values to the aggregator field value. To get models, you can use the `M\u003cModelType\u003e()` method available on the function's argument.\n    - `To\u003cModelType\u003e()` and `With()` are used to define a function that transforms the aggregator field value to a model field value. If there are several model fields, each field needs its separate configuration. First you select the target model field with `To\u003cModelType\u003e()`, then define the transform function using `With()`.\n    - `Using\u003cModelType\u003e()` defines all model fields used by the aggregator field. It allows for preloading capabilities. I'm not sure if it's necessary because the information can be inferred from `To\u003cModelType\u003e()` configs, but it'll stay for now. If a field uses several models, several `Using()` configs are needed.\n\n## Usage\nAll rules of using models apply to aggregators, including`Commit()` and `Preload()` (here called `PreloadAgg()` because of reasons). Like in models, you instantiate the `AggregatorProvider` and generic variations of it.\n\n## Nested aggregators\nNesting aggregators is supported, therefore providing SQL JOIN-like functionality. If a model contains a field with ID of another model, this situation can be mapped to nested aggregators. For example (setter and getter implementations omitted):\n```csharp\nclass ItemModel : LazyModel\n{\n    public string Description { ... }\n    public ItemModel(Id id, IDBCollection collection)\n        : base(id, collection)\n    {}\n}\n```\n```csharp\nclass ItemListingModel : LazyModel\n{\n    public string Title { ... }\n    public Id ItemId { ... }\n    public ItemListingModel(Id id, IDBCollection collection)\n        : base(id, collection)\n    {}\n}\n```\n```csharp\nclass Item : LazyAggregator\n{\n    public string Description { ... }\n    public Item(IAggregatorProvider provider, ItemModel item)\n        : base(provider, item)\n    static Item()\n    {\n        Using\u003cItem, ItemModel\u003e();\n    }\n}\n```\n```csharp\nclass ItemListing : LazyAggregator\n{\n    public string Title { ... }\n    public Item Item { ... }\n    public ItemListing(\n        IAggregatorProvider provider,\n        ItemListingModel listing)\n        : base(provider, listing)\n    {}\n    static ItemListing()\n    {\n        Using\u003cItemListing, ItemListingModel\u003e();\n        Setup\u003cItemListing\u003e()\n            .ForeignAggregator(x =\u003e x.Item)\n                .IdFrom\u003cItemListingModel\u003e(x =\u003e x.ItemId);\n    }\n}\n```\nHere, the `ItemListingModel` contains an `Id` of another model, `ItemModel`. In the aggregators, the field is mapped by selecting the foreign aggregator field and then providing information where to find the aggregator's `Id`.\n\nAnd it works:\n```csharp\nvar description = itemListing.Item.Description\n```\nwill retrieve the description by first retrieving the `Id` of the foreign aggregator from the first one, and then retrieving the Description from that.\n\nUsing `Commit()' on an aggregator also commits any changes made in its nested aggregators. Specifying the nested aggregator field in `Preload()` will preload the whole nested aggregator (and its nested ones, recursively).\n\n# Array support //TODO\nModels often contain array fields. Treating them as whole values is often impractical because they tend to get quite big. To load arrays lazily, declare them as `LazyList\u003cT\u003e`. This gives you:\n\n* Adding (appending) items lazily with the `Append()` method\n* Removing items lazily with the `Remove()` method\n* Lazy loading and setting elements by index\n* Querying for size without retrieving the array with `Count()`\n\nIt is also possible to make arrays of aggregators by specifying a `LazyList\u003cId\u003e` field in a model. It is mapped to aggregator list using a special config method (TODO).\n\n# Best practices\nTrellis enables composing functions that operate on models and aggregators without worrying about loading the data from database and explicit database calls that obfuscate application logic. A common use would be:\n```csharp\naggregator.Preload(\u003csome fields\u003e);\n\n// operate on aggregator\nSomeTransformation(aggregator);\nSomeOperation(aggregator, something);\nAnotherTransformation(aggregator);\n\naggregator.Commit();\n```\nThe `Preload()` call is never mandatory and the code will work without worrying about which fields exactly are used by the transformations. At the same time, it's easy to speed up the code with `Preload()`.\n\n# TODOs\n* Make Trellis all-async to make creating MongoDB adapter possible\n* Finish Array support and loading Arrays of aggregators\n* Fix bugs","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmsz%2Ftrellis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmsz%2Ftrellis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmsz%2Ftrellis/lists"}