1"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[8061],{55153(e,n,r){r.r(n),r.d(n,{assets:()=>l,contentTitle:()=>d,default:()=>y,frontMatter:()=>o,metadata:()=>c,toc:()=>p});var i=r(86070),s=r(81753),t=r(43292),a=r(53810);const o={title:"Repository Pattern",sidebar_position:1,description:"Use RCommon's provider-agnostic repository interfaces for async CRUD, paging, eager loading, and soft delete across EF Core, Dapper, and Linq2Db without coupling to a specific ORM."},d="Repository Pattern",c={id:"persistence/repository-pattern",title:"Repository Pattern",description:"Use RCommon's provider-agnostic repository interfaces for async CRUD, paging, eager loading, and soft delete across EF Core, Dapper, and Linq2Db without coupling to a specific ORM.",source:"@site/versioned_docs/version-3.2.1/persistence/repository-pattern.mdx",sourceDirName:"persistence",slug:"/persistence/repository-pattern",permalink:"/docs/persistence/repository-pattern",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/versioned_docs/version-3.2.1/persistence/repository-pattern.mdx",tags:[],version:"3.2.1",sidebarPosition:1,frontMatter:{title:"Repository Pattern",sidebar_position:1,description:"Use RCommon's provider-agnostic repository interfaces for async CRUD, paging, eager loading, and soft delete across EF Core, Dapper, and Linq2Db without coupling to a specific ORM."},sidebar:"docsSidebar",previous:{title:"Persistence",permalink:"/docs/category/persistence"},next:{title:"Specifications",permalink:"/docs/persistence/specifications"}},l={},p=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Usage",id:"usage",level:2},{value:"Injecting repositories",id:"injecting-repositories",level:3},{value:"Create",id:"create",level:3},{value:"Read",id:"read",level:3},{value:"Paging",id:"paging",level:3},{value:"Eager loading",id:"eager-loading",level:3},{value:"Update",id:"update",level:3},{value:"Delete",id:"delete",level:3},{value:"Turning off change tracking",id:"turning-off-change-tracking",level:3},{value:"Provider Comparison",id:"provider-comparison",level:2},{value:"API Summary",id:"api-summary",level:2}];function h(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"repository-pattern",children:"Repository Pattern"}),"\n",(0,i.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(n.p,{children:["The repository pattern in RCommon provides a uniform, provider-agnostic abstraction over data access. Rather than coupling your domain and application code to a specific ORM or database technology, you program against interfaces from ",(0,i.jsx)(n.code,{children:"RCommon.Persistence.Crud"}),". Swapping from Entity Framework Core to Dapper (or any other supported provider) requires only a configuration change at the composition root."]}),"\n",(0,i.jsx)(n.p,{children:"The abstraction hierarchy works as follows:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"IReadOnlyRepository<TEntity>"})," \u2014 async query methods"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"IWriteOnlyRepository<TEntity>"})," \u2014 async write methods (add, update, delete)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"ILinqRepository<TEntity>"})," \u2014 combines read, write, and exposes ",(0,i.jsx)(n.code,{children:"IQueryable<TEntity>"})," plus paging helpers"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"IGraphRepository<TEntity>"})," \u2014 extends ",(0,i.jsx)(n.code,{children:"ILinqRepository<TEntity>"})," with change-tracking control; used with ORM providers such as EF Core"]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["All repository interfaces require the entity to implement ",(0,i.jsx)(n.code,{children:"IBusinessEntity"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsxs)(n.p,{children:["Install the core persistence package. You will also need at least one provider package (see ",(0,i.jsx)(n.a,{href:"./efcore",children:"EF Core"}),", ",(0,i.jsx)(n.a,{href:"./dapper",children:"Dapper"}),", or ",(0,i.jsx)(n.a,{href:"./linq2db",children:"Linq2Db"}),")."]}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Persistence"}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Repositories are registered automatically when you configure a provider. Call ",(0,i.jsx)(n.code,{children:"WithPersistence<TBuilder>"})," in your application startup:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\n .WithPersistence<EFCorePersistenceBu
1ilder>(ef =>\n {\n ef.AddDbContext<LeaveManagementDbContext>(\n "LeaveManagement",\n options => options.UseSqlServer(connectionString));\n\n ef.SetDefaultDataStore(ds =>\n ds.DefaultDataStoreName = "LeaveManagement");\n });\n'})}),"\n",(0,i.jsxs)(n.p,{children:["When multiple data stores are registered you select which one a repository targets by setting ",(0,i.jsx)(n.code,{children:"DataStoreName"})," on the repository instance:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'public class CreateLeaveTypeCommandHandler\n{\n private readonly IGraphRepository<LeaveType> _repository;\n\n public CreateLeaveTypeCommandHandler(IGraphRepository<LeaveType> repository)\n {\n _repository = repository;\n _repository.DataStoreName = "LeaveManagement"; // matches the name used in AddDbContext\n }\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(n.h3,{id:"injecting-repositories",children:"Injecting repositories"}),"\n",(0,i.jsxs)(n.p,{children:["Inject the interface that matches your needs. For most domain/application code use ",(0,i.jsx)(n.code,{children:"IGraphRepository<TEntity>"})," when the provider is EF Core, or ",(0,i.jsx)(n.code,{children:"ILinqRepository<TEntity>"})," for Linq2Db. Use ",(0,i.jsx)(n.code,{children:"IReadOnlyRepository<TEntity>"})," or ",(0,i.jsx)(n.code,{children:"IWriteOnlyRepository<TEntity>"})," when you want to enforce narrower contracts."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class OrderService\n{\n private readonly IGraphRepository<Order> _orders;\n\n public OrderService(IGraphRepository<Order> orders)\n {\n _orders = orders;\n }\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"create",children:"Create"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"var order = new Order { CustomerId = customerId, Total = 99.99m };\nawait _orders.AddAsync(order, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.p,{children:"Add multiple entities in one call:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"await _orders.AddRangeAsync(newOrders, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.h3,{id:"read",children:"Read"}),"\n",(0,i.jsx)(n.p,{children:"Find by primary key:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"var order = await _orders.FindAsync(orderId, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.p,{children:"Find with a lambda expression:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"ICollection<Order> pending = await _orders.FindAsync(\n o => o.Status == OrderStatus.Pending, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.p,{children:"Find a single entity or default:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"Order? draft = await _orders.FindSingleOrDefaultAsync(\n o => o.CustomerId == customerId && o.Status == OrderStatus.Draft,\n cancellationToken);\n"})}),"\n",(0,i.jsx)(n.p,{children:"Check existence:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"bool exists = await _orders.AnyAsync(o => o.Id == orderId, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.p,{children:"Count:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"long count = await _orders.GetCountAsync(o => o.Status == OrderStatus.Pending, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.h3,{id:"paging",children:"Paging"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"ILinqRepository<TEntity>"})," and ",(0,i.jsx)(n.code,{children:"IGraphRepository<TEntity>"})," include paged query methods that return ",(0,i.jsx)(n.code,{children:"IPaginatedList<TEntity>"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"IPaginatedList<Order> page = await _orders.FindAsync(\n expression: o => o.CustomerId == customerId,\n orderByExpression: o => o.DateCreated,\n orderByAscending: false,\n pageNumber: 2,\n pageSize: 20,\n token: cancellationToken);\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Or pass a ",(0,i.jsx)(n.code,{children:"PagedSpecification<TEntity>"})," (see ",(0,i.jsx)(n.a,{href:"./specifications",children:"Specifications"}),"):"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"var spec = new PagedSpecification<Order>(\n o => o.CustomerId == customerId,\n o => o.DateCreated,\n orderByAscending: false,\n pageNumber: 1,\n pageSize: 10);\n\nIPaginatedList<Order> page = await _orders.FindAsync(spec, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.h3,{id:"eager-loading",children:"Eager loading"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"ILinqRepository<TEntity>"})," exposes fluent ",(0,i.jsx)(n.code,{children:"Include"}
1)," / ",(0,i.jsx)(n.code,{children:"ThenInclude"})," methods:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"var orders = await _orders\n .Include(o => o.Lines)\n .FindAsync(o => o.CustomerId == customerId, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.h3,{id:"update",children:"Update"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"order.Status = OrderStatus.Shipped;\nawait _orders.UpdateAsync(order, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.h3,{id:"delete",children:"Delete"}),"\n",(0,i.jsxs)(n.p,{children:["Auto-detects soft delete if the entity implements ",(0,i.jsx)(n.code,{children:"ISoftDelete"}),"; otherwise performs a physical delete:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"await _orders.DeleteAsync(order, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.p,{children:"Bulk delete by expression:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"int affected = await _orders.DeleteManyAsync(\n o => o.Status == OrderStatus.Cancelled, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.p,{children:"Override soft-delete behaviour explicitly:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"// Force physical delete even when ISoftDelete is implemented\nawait _orders.DeleteAsync(order, isSoftDelete: false, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.h3,{id:"turning-off-change-tracking",children:"Turning off change tracking"}),"\n",(0,i.jsxs)(n.p,{children:["When ",(0,i.jsx)(n.code,{children:"IGraphRepository<TEntity>"})," is used with EF Core you can disable tracking for read-only scenarios:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"_orders.Tracking = false;\nvar readOnlyOrders = await _orders.FindAsync(o => o.Status == OrderStatus.Active, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.h2,{id:"provider-comparison",children:"Provider Comparison"}),"\n",(0,i.jsx)(a.A,{title:"Repository Provider Capabilities",features:["IGraphRepository","ILinqRepository","IReadOnlyRepository","IWriteOnlyRepository","ISqlMapperRepository","Paging","Eager loading","Soft delete","Change tracking"],providers:[{name:"EF Core",packageName:"RCommon.EfCore",features:{IGraphRepository:!0,ILinqRepository:!0,IReadOnlyRepository:!0,IWriteOnlyRepository:!0,ISqlMapperRepository:!1,Paging:!0,"Eager loading":!0,"Soft delete":!0,"Change tracking":!0}},{name:"Dapper",packageName:"RCommon.Dapper",features:{IGraphRepository:!1,ILinqRepository:!1,IReadOnlyRepository:!0,IWriteOnlyRepository:!0,ISqlMapperRepository:!0,Paging:!1,"Eager loading":!1,"Soft delete":!0,"Change tracking":!1}},{name:"Linq2Db",packageName:"RCommon.Linq2Db",features:{IGraphRepository:!1,ILinqRepository:!0,IReadOnlyRepository:!0,IWriteOnlyRepository:!0,ISqlMapperRepository:!1,Paging:!0,"Eager loading":!0,"Soft delete":!0,"Change tracking":!1}}]}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Interface"}),(0,i.jsx)(n.th,{children:"Purpose"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IReadOnlyRepository<TEntity>"})}),(0,i.jsxs)(n.td,{children:["Async query operations: ",(0,i.jsx)(n.code,{children:"FindAsync"}),", ",(0,i.jsx)(n.code,{children:"FindSingleOrDefaultAsync"}),", ",(0,i.jsx)(n.code,{children:"AnyAsync"}),", ",(0,i.jsx)(n.code,{children:"GetCountAsync"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IWriteOnlyRepository<TEntity>"})}),(0,i.jsxs)(n.td,{children:["Async write operations: ",(0,i.jsx)(n.code,{children:"AddAsync"}),", ",(0,i.jsx)(n.code,{children:"AddRangeAsync"}),", ",(0,i.jsx)(n.code,{children:"UpdateAsync"}),", ",(0,i.jsx)(n.code,{children:"DeleteAsync"}),", ",(0,i.jsx)(n.code,{children:"DeleteManyAsync"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ILinqRepository<TEntity>"})}),(0,i.jsxs)(n.td,{children:["Combines read + write + ",(0,i.jsx)(n.code,{children:"IQueryable<TEntity>"})," + paging (",(0,i.jsx)(n.code,{children:"FindAsync"})," with ",(0,i.jsx)(n.code,{children:"IPaginatedList"}),") + ",(0,i.jsx)(n.code,{children:"Include"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IGraphRepository<TEntity>"})}),(0,i.jsxs)(n.td,{children:["Extends ",(0,i.jsx)(n.code,{children:"ILinqRepository"})," with ",(0,i.jsx)(n.code,{children:"Tracking"})," property for change-tracking control"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISqlMapperRepository<TEntity>"})}),(0,i.jsxs)(n.td,{children:["Raw-SQL repository for Dapper \u2014 exposes ",(0,i.jsx)(n.code,{children:"QueryAsync"})," and ",(0,i.jsx)(n.code,{children:"ExecuteAsync"})," directly"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"INamedDataSource"})}),(0,i.jsxs)(n.td,{children:["Base interface exposing ",(0,i.jsx)(n.code,{children:"DataStoreName"})," property, inherited by all repository interfaces"]})]})]})]})]})}function y(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},43292(e,n,r){r.d(n,{A:()=>l});var i=r(30758);const s="container_xjrG",t="label_Y4p8",a="commandRow_FY5I",o="command_m7Qs",d="copyButton_u1GK";var c=r(86070);function l({packageName:e,version:n}){const[r,l]=(0,i.useState)(!1),p=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:s,children:[(0,c.jsx)("div",{className:t,children:"NuGet Package"}),(0,c.jsxs)("div",{className:a,children:[(0,c.jsx)("code",{className:o,children:p}),(0,c.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(p),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},53810(e,n,r){r.d(n,{A:()=>c});r(30758);const i="container_DVa1",s="title_T4Zt",t="table_soKt",a="featureCell_LaXo",o="packageRow_ivBG";var d=r(86070);function c({title:e,features:n,providers:r}){return(0,d.jsxs)("div",{className:i,children:[(0,d.jsx)("h3",{className:s,children:e}),(0,d.jsxs)("table",{className:t,children:[(0,d.jsx)("thead",{children:(0,d.jsxs)("tr",{children:[(0,d.jsx)("th",{children:"Feature"}),r.map(e=>(0,d.jsx)("th",{children:e.name},e.name))]})}),(0,d.jsxs)("tbody",{children:[n.map(e=>(0,d.jsxs)("tr",{children:[(0,d.jsx)("td",{children:e}),r.map(n=>(0,d.jsx)("td",{className:a,children:"boolean"==typeof n.features[e]?n.features[e]?"\u2705":"\u274c":n.features[e]||"\u2014"},n.name))]},e)),(0,d.jsxs)("tr",{className:o,children:[(0,d.jsx)("td",{children:(0,d.jsx)("strong",{children:"Package"})}),r.map(e=>(0,d.jsx)("td",{children:(0,d.jsx)("code",{children:e.packageName})},e.name))]})]})]})]})}},81753(e,n,r){r.d(n,{R:()=>a,x:()=>o});var i=r(30758);const s={},t=i.createContext(s);function a(e){const n=i.useContext(t);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),i.createElement(t.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.