{"id":20765353,"url":"https://github.com/fluxera/fluxera.repository","last_synced_at":"2025-08-29T17:18:13.293Z","repository":{"id":37854221,"uuid":"435266282","full_name":"fluxera/Fluxera.Repository","owner":"fluxera","description":"A generic repository implementation.","archived":false,"fork":false,"pushed_at":"2025-02-05T17:10:18.000Z","size":1771,"stargazers_count":19,"open_issues_count":6,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-09T20:03:35.950Z","etag":null,"topics":["ddd","ddd-architecture","ddd-patterns","domain-driven-design","dotnet","dotnet7","dotnet8","expressions","generic-repository","linq","repository","repository-pattern"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fluxera.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"2021-12-05T20:03:00.000Z","updated_at":"2025-03-27T09:09:32.000Z","dependencies_parsed_at":"2023-11-23T15:27:40.869Z","dependency_job_id":"675b1692-1dfc-402b-bd56-efc270d4991d","html_url":"https://github.com/fluxera/Fluxera.Repository","commit_stats":null,"previous_names":[],"tags_count":87,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluxera%2FFluxera.Repository","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluxera%2FFluxera.Repository/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluxera%2FFluxera.Repository/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluxera%2FFluxera.Repository/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fluxera","download_url":"https://codeload.github.com/fluxera/Fluxera.Repository/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248103864,"owners_count":21048245,"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":["ddd","ddd-architecture","ddd-patterns","domain-driven-design","dotnet","dotnet7","dotnet8","expressions","generic-repository","linq","repository","repository-pattern"],"created_at":"2024-11-17T11:16:23.143Z","updated_at":"2025-04-09T20:03:45.517Z","avatar_url":"https://github.com/fluxera.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://dev.azure.com/fluxera/Foundation/_apis/build/status/GitHub/fluxera.Fluxera.Repository?branchName=main\u0026stageName=BuildAndTest)](https://dev.azure.com/fluxera/Foundation/_build/latest?definitionId=88\u0026branchName=main)\n\n# Fluxera.Repository\nA generic repository implementation.\n\nThis repository implementation hides the actual storage implementation from the user.\nThe only part where the abstraction leaks storage specifics is the configuration of\na storage specific repository implementation.\n\nThe repository can be used with or without a specialized implementation, f.e. one can\njust use one of the provided interfaces ```IRepository``` or ```IReadOnlyRepository```.\nThe repository implementation is async from top to bottom.\n\n```C#\npublic class PersonController : ControllerBase \n{\n    private readonly IRepository\u003cPerson\u003e repository;\n\n    public PersonController(IRepository\u003cPerson\u003e repository)\n    {\n        this.repository = repository\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task\u003cIActionResult\u003e Get(string id)\n    {\n        Person result = await this.repository.GetAsync(id);\n        return this.Ok(result);\n    }\n}\n```\n\nIf you prefer to create a specialized interface and implementation you can do so, but you\nhave to register the service yourself.\n\n```C#\npublic interface IPersonRepository : IRepository\u003cPerson, Guid\u003e\n{\n}\n\npublic class PersonRepository : Repository\u003cPerson, Guid\u003e, IPersonRepository\n{\n    public PersonRepository(IRepository\u003cPerson, Guid\u003e innerRepository)\n        : base(innerRepository)\n    {\n    }\n}\n```\n\nAs you can see, you have to provide the ```IRepository\u003cT, TKey\u003e``` as dependency to the constructor\nor your specialized repository. The overall architecture of the repository uses the decorator pattern\nheavily to split up the many features around the storage implementation, to keep things simple and\nhave every decorator layer only impolement a single responseibility. Your specialized repository then\nacts as the outermost layout of decorators.\n\n## Supported Storages\n\n- Entity Framework Core\n- LiteDB\n- MongoDB\n- In-Memory (for testing and prototyping only)\n\n## Additional Features\n\nBesides to being able top perform CRUD operation with the underlying data storage, the repository\nimplementation provides several additional features.\n\n### Traits\n\nSometimes you don't want to expose the complete repository interface to you specialized implementations\nand sometimes even the ```IReadOnlyRepository``` may be too much. For those cases you can just use \nthe trait interfaces yo want to support.\n\n- **```ICanAdd```**\n  Provides methods to add items.\n- **```ICanAggregate```**\n  Provides methods to aggregate results, like ```Average``` and ```Sum```.\n- **```ICanFind```**\n  Provides methods to find single and multiple results.\n- **```ICanGet```**\n  Provides methods to get items by ID.\n- **```ICanRemove```**\n  Provides methods to remove items.\n- **```ICanUpdate```**\n  Provides methods to update items.\n\nUsing this set of interfaces you cann create a specialized repository interface how you see fit.\n\n```C#\npublic interface IPersonRepository : ICanGet\u003cPerson, Guid\u003e, ICanAggregate\u003cPerson, Guid\u003e\n{\n}\n```\n\n### Specifications\n\nIn the most basic form you can execute find queries using expressions that provide the predicate\nto satify by the result items. But this may lead to queries cluttered around your application,\nmaybe duplicating code in several places. Updating queries may then become cumbersome in the future.\nTo prevent this from happening you can create specialized specification classes that encapsulate\nqueries, or parts of queries and can be re-used in your application. Any specification class can \nbe combines with others using operations like ```And```, ```Or```, ```Not```.\n\nYou can f.e. have a specification that finds all persons by name and another one that finds all\npersons by age.\n\n```C#\npublic sealed class PersonByNameSpecification : Specification\u003cPerson\u003e\n{\n    private readonly string name;\n\n    public PersonByNameSpecification(string name)\n    {\n        this.name = name;\n    }\n\n    protected override Expression\u003cFunc\u003cPerson, bool\u003e\u003e BuildQuery()\n    {\n        return x =\u003e x.Name == this.name;\n    }\n}\n\npublic sealed class PersonByAgeSpecification : Specification\u003cPerson\u003e\n{\n    private readonly int age;\n\n    public PersonByAgeSpecification(int age)\n    {\n        this.age = age;\n    }\n\n    protected override Expression\u003cFunc\u003cPerson, bool\u003e\u003e BuildQuery()\n    {\n        return x =\u003e x.Age == this.age;\n    }\n}\n```\n\nYou can combine those to find any person with a specific name and age.\n\n```C#\nPersonByNameSpecification byNameQuery = new PersonByNameSpecification(\"Tester\");\nPersonByAgeSpecification byAgeQuery = new PersonByAgeSpecification(35);\nISpecification\u003cPerson\u003e query = byNameQuery.And(byAgeQuery);\n```\n\n### Interception\n\nSometimes you may want to execute actions before or after you execute methods of the repository.\nYou can do that using the ```IInterceptor``` interface. All you have to do is implement this interface\nand register the interceptor when configuring the repository. Your interceptor will then execute the\nmethods before and after repository calls.\n\nYou can use this feature f.e. to set audit timesstamps (CreatedAt, UpdatedAt, ...) or to implement\nmore complex szenarios like multi-tenecy based on a discriminator colum. You can modify the queries\nthat should be sent to the underlying storage. If the interceptor feature is enabled (i.e at least\none custom interceptor is registered) it makes sure that any query by ID is redirected to a predicate\nbased method, so you are sure that even a get-by-id will benefit from you modifiing the predicate.\n\n### Query Options\n\nTo control how you query data is returned, you can use the ```QueryOptions``` to create sorting and\npaging options that will be applied to the base-query on execution.\n\n## Repository Decorators Hierarchy\n\nThe layers of decorators a executed in the following order.\n\n- **Diagnostics**\n  This decorator produces diagnostic events using ```System.Diagnostic``` that can be instrumented by telemetry systems.\n- **Exception Logging**\n  This decorator just catches every exception that may occur then logs the exception\n  and throws it again.\n- **Guards**\n  This decorator checks the inputs for sane values and checks if the repository instance was\n  disposed and throws corresponding exception.\n- **Validation**\n  This decorator validates the inputs to ```Add``` and ```Update``` methods for validity using\n  the configures validators.\n- **Caching**\n  If the caching is enabled this decorator manages the handling of the cache. It tries to get\n  the result of a query from the cache and if none was found executes the query and stores the\n  result, in the cache.\n- **Domain Events**\n  This decorator dispatches added domain events after an item was added, updated or removed. \n  The exact time of execution depends on the configuration of the repository. If UoW is enabled \n  the domain events are dispatched to a outbox queue and flushed to the event handlers when the\n  UoW saves the work. If the UoW is disabled the domain events are immediately dispatched to the\n  event handlers after the item was added, updated or removed.\n- **Data Storage**\n  This is the base layer around wich all decorators are configures. This is the storage specific\n  repository implementation.\n\n## Unit of Work\n\nThe Unit of Work (UoW) pattern is disabled by default an  can be enabled using the ```EnableUnitOfWork``` method\nof the ```IRepositoryOptionsBuilder```.\n\nWhen enabled, a simple call to, f.e. ```AddAsync(item)``` will not persist the given item instantly. The \nadd operation is added to the UoW instance and is executed when the UoW for the repository saves the changes.\n\n```C#\nawait this.repository.AddAsync(new Company\n{\n    Name = \"First Company\",\n    LegalType = LegalType.LimitedLiabilityCompany\n});\n\nawait this.repository.AddAsync(new Company\n{\n    Name = \"Second Company\",\n    LegalType = LegalType.Corporation\n});\n\nawait this.unitOfWork.SaveChangesAsync();\n```\n\nDue to the fact that this library supports multiple, different repositories at the same time, a UoW \ninstance can not be obtained directly using dependency injection. You can get a UoW instance from\nthe ```IUnitOfWorkFactory``` with the name of the repository.\n\n```C#\nthis.unitOfWork = unitOfWorkFactory.CreateUnitOfWork(\"MongoDB\");\n```\n\n## OpenTelemetry\n\nThe repository produces ```Activity``` events using ```System.Diagnistic```. Those events are used\nmy the OpenTelemetry integration to support diagnostic insights. To enable the support for OpenTelemetry\njust add the package ```Fluxera.Repository.OpenTelemetry``` to your OpenTelemetry enabled application\nand add the instrumentation for the Repository shown below.\n\n```C#\n// Configure important OpenTelemetry settings, the console exporter, and automatic instrumentation.\nbuilder.Services.AddOpenTelemetryTracing(builder =\u003e\n{\nbuilder\n    .AddConsoleExporter()\n    .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(\"WebApplication1\", \"1.0.0\"))\n    .AddHttpClientInstrumentation()\n    .AddAspNetCoreInstrumentation()\n    .AddMongoDBInstrumentation()\n    // Add the instrumentation for the Repository.\n    .AddRepositoryInstrumentation();\n});\n```\n\n## Usage\n\nYou can configure different repositories for different aggregate root types, f.e. you can have persons\nin a MongoDB and invoices in a SQL database. You can choose to omit the repository name using overloads\nthat do not accept a name parameter. In this case, the default name \"Default\" is used for the repository.\n\n```C#\n// Add the repository services to the service collection and configure the repositories.\nservices.AddRepository(builder =\u003e\n{\n    // Add default services and the repositories.\n    builder.AddMongoRepository\u003cSampleMongoContext\u003e(\"MongoDB\", options =\u003e\n    {\n        // Enable UoW for this repository.\n        options.EnableUnitOfWork();\n\n        // Configure for what aggregate root types this repository uses.\n        options.UseFor\u003cPerson\u003e();\n\n        // Enable the domain events (optional).\n        options.EnableDomainEventHandling();\n\n        // Enable validation providers (optional).\n        options.EnableValidation(validation =\u003e\n        {\n            validation.AddValidatorsFromAssembly(typeof(Person).Assembly);\n        });\n\n        // Enable caching (optional).\n        options.EnableCaching((caching =\u003e\n        {\n            caching\n                .UseStandard()\n                .UseTimeoutFor\u003cPerson\u003e(TimeSpan.FromSeconds(20));\n        });\n\n        // Enable the interceptors (optional).\n        options.EnableInterception(interception =\u003e\n        {\n            interception.AddInterceptorsFromAssembly(typeof(Person).Assembly);\n        });\n\n        // Set storage specific settings.\n        options.AddSetting(\"Mongo.ConnectionString\", \"mongodb://localhost:27017\");\n        options.AddSetting(\"Mongo.Database\", \"test\");\n    });\n});\n```\n\nStorage-specific options are configure using a repository-specific context class. \nThe following example shows the configuration of a MongoDB repository.\n\n```C#\npublic class SampleMongoContext : MongoContext\n{\n    protected override void ConfigureOptions(MongoContextOptions options)\n    {\n        options.ConnectionString = \"mongodb://localhost:27017\";\n        options.Database = \"sample\";\n    }\n}\n```\n\nThe context types are registered as scoped services in the container. The ```void ConfigureOptions(MongoContextOptions options)```\nmethod is called whenever an instance of the context is created. In a web application this will occur for\nevery request. You can then modify, f.e. the connection strings or database names to use for this context instance.\n\nThis comes in handy if you plan on implementing \"database-per-tenant\" data isolation in SaaS szenarios.\n\n## References\n\nThe [OpenTelemetry](https://opentelemetry.io/) project.\n\nThe [MongoDB C# Driver](https://github.com/mongodb/mongo-csharp-driver) project.\n\nThe [Entity Framework Core](https://github.com/dotnet/efcore) project.\n\nThe [LiteDB](https://github.com/mbdavid/LiteDB) project.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffluxera%2Ffluxera.repository","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffluxera%2Ffluxera.repository","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffluxera%2Ffluxera.repository/lists"}