+
+
+
+
\ No newline at end of file
diff --git a/assets/app.KvA41C-Y.js b/assets/app.KvA41C-Y.js
new file mode 100644
index 000000000..4be5cc60d
--- /dev/null
+++ b/assets/app.KvA41C-Y.js
@@ -0,0 +1,13 @@
+import{a9 as P,_,aa as T,o,c as r,k as u,F as b,E as y,n as v,t as A,ab as x,ac as D,r as E,d as C,X as S,a as L,R as O,ad as I,ae as j,af as f,v as m,ag as H,ah as V,ai as F,aj as R,ak as M,al as N,am as B,an as K,ao as q,ap as z,u as U,j as Y,z as G,aq as W,ar as X,as as J}from"./chunks/framework.g9eZ-ZSs.js";import{t as w}from"./chunks/theme.h7w36Qzb.js";const h={groups:{default:{vue:"Vue",knockout:"Knockout"},vue:{options:"Options API",setup:"Composition API"},"vue-bundler":{"vue-cli":"Vue CLI",vite:"Vite"}}},p=P({}),Q={props:{name:{type:String,default:"default"},isolated:{type:Boolean,default:!1},languages:{type:Object,required:!1}},data(){return{selectedLanguage:null,actualLanguages:{}}},computed:{localStorageKey(){return`vuepress-plugin-code-switcher@${this.name}`}},methods:{switchLanguage(e){if(this.isolated)return this.selectedLanguage=e;typeof localStorage<"u"&&localStorage.setItem(this.localStorageKey,e),p[this.name]=e},setConfiguredDefaultLanguages(){this.languages?this.actualLanguages=this.languages:h&&h.groups&&h.groups[this.name]&&(this.actualLanguages=h.groups[this.name]),this.selectedLanguage=Object.keys(this.actualLanguages)[0],p[this.name]||(p[this.name]=this.selectedLanguage)}},created(){if(!this.isolated){if(this.setConfiguredDefaultLanguages(),!this.actualLanguages)throw new Error('You must specify either the "languages" prop or use the "groups" option when configuring the plugin.');if(typeof localStorage<"u"){let e=localStorage.getItem(this.localStorageKey);e&&Object.keys(this.actualLanguages).indexOf(e)!==-1&&(this.selectedLanguage=e)}T(()=>this.selectedLanguage=p[this.name])}}},Z={class:"code-tabs"},ee={class:"code-tabs__nav"},te={class:"code-tabs__ul"},ae=["aria-pressed","aria-expanded","onClick"],se=["aria-selected"];function ie(e,t,a,n,i,l){return o(),r("div",Z,[u("div",ee,[u("ul",te,[(o(!0),r(b,null,y(i.actualLanguages,(d,s)=>(o(),r("li",{key:s,class:"code-tabs__li"},[u("button",{class:v(["code-tabs__nav-tab",{"code-tabs__nav-tab-active":i.selectedLanguage===s}]),"aria-pressed":i.selectedLanguage===s,"aria-expanded":i.selectedLanguage===s,onClick:Ae=>l.switchLanguage(s)},A(d),11,ae)]))),128))])]),(o(!0),r(b,null,y(i.actualLanguages,(d,s)=>x((o(),r("div",{key:s,class:v(["code-tabs-item",{"code-tabs-item__active":i.selectedLanguage===s}]),"aria-selected":i.selectedLanguage===s},[E(e.$slots,s)],10,se)),[[D,s===i.selectedLanguage]])),128))])}const ne=_(Q,[["render",ie]]);var g=new Map;async function oe(e){const t=await S(()=>import("./chunks/index.esm.VSePrB68.js"),__vite__mapDeps([]));return t.setCDN("https://unpkg.com/shiki/"),{highlighter:await t.getHighlighter({theme:"dark-plus",langs:[e]}),shiki:t}}function re(e){if(g.has(e))return g.get(e);const t=oe(e);return g.set(e,t),t}const ce=C({props:{def:{type:String,required:!0},lang:{type:String,default:"c#"},ctor:{type:[Number,String],default:null},noClass:{type:Boolean,default:!1},id:{type:String,default:null},idPrefix:{type:String,default:"member"}},data(){return{idAttr:"",html:""}},async serverPrefetch(){await this.renderHtml()},beforeMount(){var e,t;this.html=(e=this.$el)==null?void 0:e.innerHTML,this.idAttr=(t=this.$el)==null?void 0:t.id,this.html||this.$watch("def",()=>this.renderHtml(),{immediate:!0})},methods:{async renderHtml(){var e=null;this.ctor&&(e="// Also settable via constructor parameter #"+this.ctor,(this.lang=="c#"||this.lang=="csharp")&&!this.def.match(/\bset\b/)&&(e="// ONLY settable via constructor parameter #"+this.ctor));const t=(this.noClass?"":"public class x {")+(e?`
+`+e:"")+`
+`+this.def+(this.noClass?"":`
+}`);if(this.id)this.idAttr=this.id;else if(this.lang=="ts"){const s=/(?:(?:readonly|public|static|protected|private|abstract|export) )*((?:namespace )?[\w$-]+)/.exec(this.def);this.idAttr=s?this.idPrefix+"-"+s[1]:null}else if(this.lang=="c#"){const s=/ (\w+)(?:<|\(| \{|$)/.exec(this.def);this.idAttr=s?this.idPrefix+"-"+s[1]:null}if(!this.idAttr)throw new Error("Unable to compute id for Prop "+this.def);this.idAttr=this.idAttr.toLowerCase().replace(/[ &<>"']/g,"-").replace(/\$/g,"_");const{highlighter:a,shiki:n}=await re(this.lang),i=a.codeToThemedTokens(t,this.lang);this.noClass||(i.shift(),i.pop());const l=a.getTheme();let d=n.renderToHtml(i,{fg:l.fg,bg:l.bg});this.html=`${d}`}}}),le=["innerHTML","id"],ue=["id"],de={class:"shiki",style:{"line-height":"1.18","padding-top":"1px","padding-bottom":"4px"}};function he(e,t,a,n,i,l){return e.html?(o(),r("h4",{key:0,innerHTML:e.html,class:"code-prop",id:e.idAttr},null,8,le)):(o(),r("h4",{key:1,class:"code-prop",id:e.idAttr},[u("pre",de,[L(" "),u("code",null,`
+ `+A(e.def)+`
+ `,1),L(`
+ `)])],8,ue))}const pe=_(ce,[["render",he]]),ge={},fe={class:"vp-doc intellitect-footer"},me=O('
Coalesce is a free and open-source framework created by IntelliTect to fill our desire to create better apps, faster. IntelliTect is a high-end software architecture and development consulting firm based in Spokane, Washington.
If you're looking for help with your software project, whether it be a Coalesce application, other technologies, or even just an idea, reach out to us at info@intellitect.com — we'd love to start a conversation! Our clients range from Fortune 100 companies to local small businesses and non-profits.
When Coalesce maps from the your POCO objects that are returned from EF Core queries, it will follow a structure called an IncludeTree to determine what relationships to follow and how deep to go in re-creating that structure in the mapped DTOs.
Without an IncludeTree present, Coalesce will map the entire object graph that is reachable from the root object. This can often spiral out of control if there aren't any rules defining how far to go while turning this graph into a tree.
For example, suppose you had the following model with a many-to-many relationship (key properties omitted for brevity):
c#
public class Employee
+{
+ [ManyToMany("Projects")]
+ public ICollection<EmployeeProject> EmployeeProjects { get; set; }
+
+ public static IQueryable<Employee> WithProjectsAndMembers(AppDbContext db, ClaimsPrincipal user)
+ {
+ // Load all projects of an employee, as well as all members of those projects.
+ return db.Employees
+ .Include(e => e.EmployeeProjects)
+ .ThenInclude(ep => ep.Project.EmployeeProjects)
+ .ThenInclude(ep => ep.Employee);
+ }
+}
+
+public class Project
+{
+ [ManyToMany("Employees")]
+ public ICollection<EmployeeProject> EmployeeProjects { get; set; }
+}
+
+public class EmployeeProject
+{
+ public Employee Employee { get; set; }
+ public Project Project { get; set; }
+}
Now, imagine that you have five employees and five projects, with every employee being a member of every project (i.e. there are 25 EmployeeProject rows).
Your client code makes a call to the Coalesce-generated API to load Employee #1 using the custom data source:
If you're already familiar with the fact that an IncludeTree is implicitly created in this scenario, then imagine for a moment that this is not the case (if you're not familiar with this fact, then keep reading!).
After Coalesce has called your Data Sources and evaluated the EF IQueryable returned, there are now 35 objects loaded into the current DbContext being used to handle this request - the 5 employees, 5 projects, and 25 relationships.
To map these objects to DTOs, we start with the root (employee #1) and expand outward from there until the entire object graph has been faithfully re-created with DTO objects, including all navigation properties.
The root DTO object (employee #1) then eventually is passed to the JSON serializer by ASP.NET Core to formulate the response to the request. As the object is serialized to JSON, the only objects that are not serialized are those that were already serialized as an ancestor of itself. What this ultimately means is that the structure of the serialized JSON with our example scenario ends up following a pattern like this (the vast majority of items have been omitted):
Employee#1
+ EmployeeProject#1
+ Project#1
+ EmployeeProject#6
+ Employee#2
+ EmployeeProject#7
+ Project#2
+ ... continues down through all remaining employees and projects.
+ ...
+ EmployeeProject#11
+ Employee#3
+ ...
+ EmployeeProject#2
+ Project#2
+ ...
See how the structure includes the EmployeeProjects of Employee#2? We didn't write our custom data source calls to .Include in such a way that indicated that we wanted the root employee, their projects, the employees of those projects, and then the projects of those employees. But, because the JSON serializer blindly follows the object graph, that's what gets serialized. It turns out that the depth of the tree increases on the order of O(n^2), and the total size increases on the order of Ω(n!).
This is where IncludeTree comes in. When you use a custom data source like we did above, Coalesce automatically captures the structure of the calls to .Include and .ThenInclude, and uses this to perform trimming during creation of the DTO objects.
With an IncludeTree in place, our new serialized structure looks like this:
In most cases, you don't have to worry about creating an IncludeTree. When using the Standard Data Source (or a derivative), the structure of the .Include and .ThenInclude calls will be captured automatically and be turned into an IncludeTree.
However, there are sometimes cases where you perform complex loading in these methods that involves loading data into the current DbContext outside of the IQueryable that is returned from the method. The most common situation for this is needing to conditionally load related data - for example, load all children of an object where the child has a certain value of a Status property.
In these cases, Coalesce provides a pair of extension methods, .IncludedSeparately and .ThenIncluded, that can be used to merge in the structure of the data that was loaded separately from the main IQueryable.
For example:
c#
public override IQueryable<Employee> GetQuery()
+{
+ // Load all projects that are complete, and their members, into the db context.
+ Db.Projects
+ .Include(p => p.EmployeeProjects).ThenInclude(ep => ep.Employee)
+ .Where(p => p.Status == ProjectStatus.Complete)
+ .Load();
+
+ // Return an employee query, and notify Coalesce that we loaded the projects in a different query.
+ return Db.Employees
+ .IncludedSeparately(e => e.EmployeeProjects)
+ .ThenIncluded(ep => ep.Project.EmployeeProjects)
+ .ThenIncluded(ep => ep.Employee);
+}
You can also override the GetIncludeTree method of the Standard Data Source to achieve the same result:
If you have custom methods that return object data, you may also want to control the structure of the returned data when it is serialized. Fortunately, you can also use IncludeTree in these situations. Without an IncludeTree, the entire object graph is traversed and serialized without limit.
TIP
An IncludeTree can be obtained from any IQueryable by calling the GetIncludeTree extension method (using IntelliTect.Coalesce.Helpers.IncludeTree).
In situations where your root object isn't on your DbContext (see External Types), you can use Enumerable.Empty<MyNonDbClass>().AsQueryable() to get an IQueryable to start from. When you do this, you must use IncludedSeparately - the regular EF Include method won't work without a DbSet.
See the following two techniques for returning an IncludeTree from a custom method:
The easiest and most versatile way to return an IncludeTree from a custom method is to make that method return an ItemResult<T>, and then set the IncludeTree property of the ItemResult object. For example:
c#
public class Employee
+{
+ public async Task<ItemResult<ICollection<Employee>>> GetChainOfCommand(AppDbContext db)
+ {
+ IQueryable<Employee> query = db.Employees
+ .Include(e => e.Supervisor);
+
+ var ret = new List<Employee>();
+ var current = this;
+ while (current.Supervisor != null)
+ {
+ ret.Push(current);
+ current = await query.FirstOrDefaultAsync(e => e.EmployeeId == current.SupervisorId);
+ }
+
+ return new(ret, includeTree: query.GetIncludeTree());
+ }
+}
To tell Coalesce about the structure of the data returned from a model method, you can also add out IncludeTree includeTree to the signature of the method. Inside your method, set includeTree to an instance of an IncludeTree. However, this approach cannot be used on async methods, since out parameters are not allowed on async methods in C#. For example:
c#
public class Employee
+{
+ public ICollection<Employee> GetChainOfCommand(AppDbContext db, out IncludeTree includeTree)
+ {
+ IQueryable<Employee> query = db.Employees
+ .Include(e => e.Supervisor);
+
+ var ret = new List<Employee>();
+ var current = this;
+ while (current.Supervisor != null)
+ {
+ ret.Push(current);
+ current = query.FirstOrDefault(e => e.EmployeeId == current.SupervisorId);
+ }
+
+ includeTree = query.GetIncludeTree();
+
+ return ret;
+ }
+}
One important point remains regarding IncludeTree - it is not used to control the serialization of objects which are not mapped to the database, known as External Types. External Types are always put into the DTOs when encountered (unless otherwise prevented by [DtoIncludes] & [DtoExcludes] or Security Attributes), with the assumption that because these objects are created by you (as opposed to Entity Framework), you are responsible for preventing any undesired circular references.
By not filtering unmapped properties, you as the developer don't need to account for them in every place throughout your application where they appear - instead, they 'just work' and show up on the client as expected.
Note also that this statement does not apply to database-mapped objects that hang off of unmapped objects - any time a database-mapped object appears, it will be controlled by your include tree. If no include tree is present (because nothing was specified for the unmapped property), these mapped objects hanging off of unmapped objects will be serialized freely and with all circular references, unless you include some calls to .IncludedSeparately(m => m.MyUnmappedProperty.MyMappedProperty) to limit those objects down.
`,33);function u(h,m,E,b,g,F){const o=p("CodeTabs");return t(),c("div",null,[y,r(o,null,{vue:a(()=>[i]),knockout:a(()=>[d]),_:1}),C])}const A=l(D,[["render",u]]);export{I as __pageData,A as default};
diff --git a/assets/concepts_include-tree.md.tAg1rwYk.lean.js b/assets/concepts_include-tree.md.tAg1rwYk.lean.js
new file mode 100644
index 000000000..534d9d511
--- /dev/null
+++ b/assets/concepts_include-tree.md.tAg1rwYk.lean.js
@@ -0,0 +1,8 @@
+import{_ as l,D as p,o as t,c,I as r,w as a,R as n,k as s,a as e}from"./chunks/framework.g9eZ-ZSs.js";const I=JSON.parse('{"title":"Include Tree","description":"","frontmatter":{},"headers":[],"relativePath":"concepts/include-tree.md","filePath":"concepts/include-tree.md"}'),D={name:"concepts/include-tree.md"},y=n("",8),i=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"Employee"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"EmployeeViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," employee"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," EmployeeViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," Employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"DataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"WithProjectsAndMembers"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")])])])],-1),d=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," employee"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Employee"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"WithProjectsAndMembers"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")])])])],-1),C=n("",33);function u(h,m,E,b,g,F){const o=p("CodeTabs");return t(),c("div",null,[y,r(o,null,{vue:a(()=>[i]),knockout:a(()=>[d]),_:1}),C])}const A=l(D,[["render",u]]);export{I as __pageData,A as default};
diff --git a/assets/concepts_includes.md.va6Oh-E9.js b/assets/concepts_includes.md.va6Oh-E9.js
new file mode 100644
index 000000000..8e776a99e
--- /dev/null
+++ b/assets/concepts_includes.md.va6Oh-E9.js
@@ -0,0 +1,50 @@
+import{_ as p,D as a,o as r,c as i,I as l,w as o,a as e,k as s,R as t}from"./chunks/framework.g9eZ-ZSs.js";const V=JSON.parse('{"title":"Includes String","description":"","frontmatter":{},"headers":[],"relativePath":"concepts/includes.md","filePath":"concepts/includes.md"}'),D={name:"concepts/includes.md"},d=s("h1",{id:"includes-string",tabindex:"-1"},[e("Includes String "),s("a",{class:"header-anchor",href:"#includes-string","aria-label":'Permalink to "Includes String"'},"")],-1),y=s("p",null,'Coalesce provides a number of extension points for loading & serialization which make use of a concept called an "includes string" (also referred to as "include string" or just "includes").',-1),C=s("h2",{id:"includes-string-1",tabindex:"-1"},[e("Includes String "),s("a",{class:"header-anchor",href:"#includes-string-1","aria-label":'Permalink to "Includes String"'},"")],-1),u=s("p",null,"The includes string is simply a string which can be set to any arbitrary value. It is passed from the client to the server in order to customize data loading and serialization. It can be set on both the TypeScript ViewModels and the ListViewModels.",-1),h=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},", "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," person"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")])])])],-1),m=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," person"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")])])])],-1),b=t(`
The default value (i.e. no action) is the empty string.
There are a few values of includes that are either set by default in the auto-generated views, or otherwise have special meaning:
Value
Description
'none'
Setting includes to none suppresses the Default Loading Behavior provided by the Standard Data Source - The resulting data will be the requested object (or list of objects) and nothing more.
There are two C# attributes, DtoIncludes and DtoExcludes, that can be used to annotate your data model in order to customize what data gets put into the DTOs and ultimately serialized to JSON and sent out to the client.
When the database entries are returned to the client they will be trimmed based on the requested includes string and the values in DtoExcludes and DtoIncludes.
DANGER
These attributes are not security attributes - consumers of your application's API can set the includes string to any value when making a request.
Do not use them to keep certain data private - use the Security Attributes family of attributes for that.
It is important to note that the value of the includes string will match against these attributes on any of your models that appears in the object graph being mapped to DTOs - it is not limited only to the model type of the root object.
Important
DtoIncludes does not ensure that specific data will be loaded from the database. It only permits what is already loaded into the current EF DbContext to be returned from the API. See Data Sources to learn how to control what data gets loaded from the database.
public class Person
+{
+ // Don't include CreatedBy when editing - will be included for all other views
+ [DtoExcludes("Editor")]
+ public AppUser CreatedBy { get; set; }
+
+ // Only include the Person's Department when \`includes == "details"\` on the TypeScript ViewModel.
+ [DtoIncludes("details")]
+ public Department Department { get; set; }
+
+ // LastName will be included in all views
+ public string LastName { get; set; }
+}
+
+public class Department
+{
+ [DtoIncludes("details")]
+ public ICollection<Person> People { get; set; }
+}
Client code:
`,15),g=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList.$items will not contain CreatedBy nor Department objects.")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList2.items will be allowed to contain both CreatedBy and Department objects. ")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Department will be allowed to include its other Person objects.")])])])],-1),E=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}}," // objects in personList.items will not contain CreatedBy nor Department objects.")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}}," // objects in personList2.items will be allowed to contain both CreatedBy and Department objects. Department will be allowed to include its other Person objects.")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")])])])],-1),w=s("h3",{id:"properties",tabindex:"-1"},[e("Properties "),s("a",{class:"header-anchor",href:"#properties","aria-label":'Permalink to "Properties"'},"")],-1),_=t('
A comma-delimited list of values of includes on which to operate.
For DtoIncludes, this will be the values of includes for which this property will be allowed to be serialized and sent to the client.
For DtoExcludes, this will be the values of includes for which this property will not be serialized and sent to the client.
',3);function f(v,F,A,L,P,k){const n=a("CodeTabs"),c=a("Prop");return r(),i("div",null,[d,y,C,u,l(n,null,{vue:o(()=>[h]),knockout:o(()=>[m]),_:1}),b,l(n,null,{vue:o(()=>[g]),knockout:o(()=>[E]),_:1}),w,l(c,{def:"public string ContentViews { get; set; }",ctor:"1"}),e(),_])}const I=p(D,[["render",f]]);export{V as __pageData,I as default};
diff --git a/assets/concepts_includes.md.va6Oh-E9.lean.js b/assets/concepts_includes.md.va6Oh-E9.lean.js
new file mode 100644
index 000000000..9ef81d721
--- /dev/null
+++ b/assets/concepts_includes.md.va6Oh-E9.lean.js
@@ -0,0 +1,32 @@
+import{_ as p,D as a,o as r,c as i,I as l,w as o,a as e,k as s,R as t}from"./chunks/framework.g9eZ-ZSs.js";const V=JSON.parse('{"title":"Includes String","description":"","frontmatter":{},"headers":[],"relativePath":"concepts/includes.md","filePath":"concepts/includes.md"}'),D={name:"concepts/includes.md"},d=s("h1",{id:"includes-string",tabindex:"-1"},[e("Includes String "),s("a",{class:"header-anchor",href:"#includes-string","aria-label":'Permalink to "Includes String"'},"")],-1),y=s("p",null,'Coalesce provides a number of extension points for loading & serialization which make use of a concept called an "includes string" (also referred to as "include string" or just "includes").',-1),C=s("h2",{id:"includes-string-1",tabindex:"-1"},[e("Includes String "),s("a",{class:"header-anchor",href:"#includes-string-1","aria-label":'Permalink to "Includes String"'},"")],-1),u=s("p",null,"The includes string is simply a string which can be set to any arbitrary value. It is passed from the client to the server in order to customize data loading and serialization. It can be set on both the TypeScript ViewModels and the ListViewModels.",-1),h=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},", "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," person"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")])])])],-1),m=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," person"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")])])])],-1),b=t("",15),g=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList.$items will not contain CreatedBy nor Department objects.")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList2.items will be allowed to contain both CreatedBy and Department objects. ")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Department will be allowed to include its other Person objects.")])])])],-1),E=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}}," // objects in personList.items will not contain CreatedBy nor Department objects.")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}}," // objects in personList2.items will be allowed to contain both CreatedBy and Department objects. Department will be allowed to include its other Person objects.")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")])])])],-1),w=s("h3",{id:"properties",tabindex:"-1"},[e("Properties "),s("a",{class:"header-anchor",href:"#properties","aria-label":'Permalink to "Properties"'},"")],-1),_=t("",3);function f(v,F,A,L,P,k){const n=a("CodeTabs"),c=a("Prop");return r(),i("div",null,[d,y,C,u,l(n,null,{vue:o(()=>[h]),knockout:o(()=>[m]),_:1}),b,l(n,null,{vue:o(()=>[g]),knockout:o(()=>[E]),_:1}),w,l(c,{def:"public string ContentViews { get; set; }",ctor:"1"}),e(),_])}const I=p(D,[["render",f]]);export{V as __pageData,I as default};
diff --git a/assets/history.md.ux281dRc.js b/assets/history.md.ux281dRc.js
new file mode 100644
index 000000000..be8aae7e6
--- /dev/null
+++ b/assets/history.md.ux281dRc.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as a,R as i}from"./chunks/framework.g9eZ-ZSs.js";const g=JSON.parse(`{"title":"Welcome to Coalesce's documentation!","description":"","frontmatter":{},"headers":[],"relativePath":"history.md","filePath":"history.md"}`),o={name:"history.md"},s=i('
Coalesce is a framework based on ASP.NET Core that makes rapidly building awesome websites much easier. A project that would take a 3 months to complete now takes 1 month. We built this because we got tired of writing all the boiler plate code that is necessary to make amazing sites.
It does this by allowing developers to focus on the creative aspects of the solution. The more mundane parts are generated automatically. This means that you get to focus on data modeling, business logic and front-end development. Coalesce does the plumbing.
Here is a typical workflow
Build an EF Core data model with business logic
Coalesce generates controllers, TypeScript view models, API and view model documentation, and admin pages/examples
Build an interactive and intuitive user experience
Coalesce was designed to create line-of-business applications. It provides a more customizable and maintainable alternative to off the shelf customizable products like SharePoint and Sales Force.
You should consider using Coalesce if your project:
Is small to medium size (1-200 classes)
Requires an interactive user experience
Has data entry requirements, especially forms, tables, etc.
Needs to get started quickly with functional prototypes that can become production software
Coalesce is specifically designed to meet the needs of web developers. However, there are lots of ways to do this. We have made a set of decisions which we believe makes for a great development experience
ASP.NET Core: there is no intent to back port this to an earlier version
EF Core for the object relational mapper
Currently uses the full framework because .NET Core doesn't supported the required functionality, yet
Knockout for client-side data binding
Business logic most easily lives in the model classes
Coalesce is designed for relational databases. This might change in the future, but not until we have a compelling use case.
After you create your classes and the EF data context, Coalesce uses this information to generate code. When the Coalesce CLI (command line interface) is run, the following things happen:
The model is validated to ensure that all the Coalesce specific requirements are met. This includes things like ensuring that all classes have a primary key assigned, validating that linked child objects have a key to their parent, etc. If issues are found, generation stops and the errors are displayed with advise to fix the issues.
The core files needed for Coalesce are copied to the target project. This includes TypeScript base classes, customizable templates, and other files for extension points. Each file is copied twice, once as a file that can be modified in the project and once as an original file. This ensures that if any changes are made by the user these files Coalesce will not overwrite the your changes.
The API controllers are generated. One is generated for each object. This includes methods that get a list of items, get a specific item, save an item, etc.
The TypeScript view models are created. There are two view models for each object. One is a list view model which allows for getting an displaying lists of a type of object. This includes full functionality to sort, filter, search, page, etc. Additionally, a view model that represents the individual object is also created. This has all the properties and methods of the server side object. This is basically a client-side proxy object for representing and manipulating the object on the server side. These objects seamlessly use the API controllers to interact with the server.
Next, the View controller are created. One is create for each model class and provide a tabular view, a card view (for mobile), an editor, and documentation.
Finally, the CSHTML views for the controller are created. These are the actual CSHTML for the above controllers. These not only provide administrative view and editing features, but also serve as an example of how to use the framework
Here are a few things we have found helpful when using Coalesce
Learn and embrace the Coalesce paradigm and work with it rather than trying to do things another way.
Following what we refer to as the 'well worn path' is very helpful. Try to stick to standard ways to do things rather than trying to use esoteric features.
Keep your models as consistent and straightforward as possible. Use relational modeling best practices.
Remember that public methods on your class models are added to the client side view models and this makes calling business logic from the client really easy.
Don't be afraid to fall back to building parts of your site using traditional methods. Coalesce isn't right for everything. But, honestly, we have only done this a few times, like 3.
In 2014 several developers from IntelliTect got together to talk about our craft. There were lots of different backgrounds, but recently we had all been writing web code in C#. We discussed things we enjoyed and things we dreaded. There was an underlying commitment to providing customers with great sites at a reasonable cost. However, those things often seemed at odds because of the complexity of web development.
For example, writing AJAX drop down lists with type ahead takes quite a bit of plumbing. Layer onto this the need for view models that allow for validation and saving as the user moves from field to field. We absolutely want want to deliver visually pleasing sites with complex UI paradigms. However, all this excellence adds up: complex view models, complex APIs, data binding, ugh.
Then there is that sinking feeling when you have to add another class to the project knowing that you are going to need to create all this yet again and you consider taking short cuts. Will there really be more than about 20 items in this table, maybe we don't need paging. Inevitably, the customer asks for admin screens. We consider giving them SQL Server Management Console and then consider using the built in ASP.NET list and editor pages. Better sense wins out and we end up spending two weeks building slick admin pages with paging, searching, sorting, etc.
That evening we starting talking about the things we loved to do:
Data modeling
Figuring out and writing business logic
Working with customers
Making cool user interfaces
Creating something new and awesome
We also lists things that we didn't enjoy
Writing the same controller again
Creating a view model for a class that is similar but different from another one in the project
Putting sorting and paging on every admin page
Basically doing anything that feels repetitive or boilerplate
Over the next few months we talked about this issue, but couldn't find the right abstraction. We talked about other solutions that solve parts of the problem and considered putting together something from several pieces. Nothing felt unified and we ended up with leaky abstractions. We needed some way to divide the problem so that we could build the fun stuff and have something generate the boring stuff. This solution needed to be robust enough to satisfy our customer's needs and also be of use to developers without their needing to know the inner workings of the system.
What if we could build the models and business logic and have a tool build everything except the UI? There are great tools like Entity Framework for modeling and good tooling for minimizing duplicate code in user interfaces. And so Coalesce was born, a tool that would bring together the backend and front end development. '
Coalesce takes Entity Framework Core models and builds controllers, TypeScript view models, and admin pages automatically. These are built in a general way so that they can be applied to many different scenarios. There will always be pages that need to be written by hand and we intentionally don't support many edge cases in order to keep things simple. There is nothing wrong with building something by hand.
We have been using Coalesce for many of our web projects with great success. Typically, a project is taking about 1/3 the time it was taking before once developers ramp up. The ramp up on Coalesce has typically been a couple of days. We realized that in order for Coalesce to be useful it need to be intuitive to use and easy to understand. We have intentionally used simple paradigms to minimize the learning curve. There are complex bits, but hopefully, those are well hidden and documented as needed.
',37),n=[s];function l(r,d,h,c,u,m){return t(),a("div",null,n)}const f=e(o,[["render",l]]);export{g as __pageData,f as default};
diff --git a/assets/history.md.ux281dRc.lean.js b/assets/history.md.ux281dRc.lean.js
new file mode 100644
index 000000000..6f20c4880
--- /dev/null
+++ b/assets/history.md.ux281dRc.lean.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as a,R as i}from"./chunks/framework.g9eZ-ZSs.js";const g=JSON.parse(`{"title":"Welcome to Coalesce's documentation!","description":"","frontmatter":{},"headers":[],"relativePath":"history.md","filePath":"history.md"}`),o={name:"history.md"},s=i("",37),n=[s];function l(r,d,h,c,u,m){return t(),a("div",null,n)}const f=e(o,[["render",l]]);export{g as __pageData,f as default};
diff --git a/assets/index.md.J5qzQ5SM.js b/assets/index.md.J5qzQ5SM.js
new file mode 100644
index 000000000..a34f8d03a
--- /dev/null
+++ b/assets/index.md.J5qzQ5SM.js
@@ -0,0 +1 @@
+import{_ as t,D as o,o as a,c as r,I as n}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse(`{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"Coalesce","text":"Accelerated Web App Development","tagline":"ASP.NET Core • EF Core • Vue.js • TypeScript","image":{"src":"/coalesce-icon-color.svg","alt":"Coalesce"},"actions":[{"theme":"brand","text":"Introduction","link":"/introduction"},{"theme":"alt","text":"Get Started","link":"/stacks/vue/getting-started"}]},"features":[{"title":"🖨 Code Generated","details":"Design your data model and build awesome pages. Coalesce generates the boring parts in the middle."},{"title":"🧩 Extensible","details":"All functionality in Coalesce is configurable or overridable. You'll never be boxed in or get stuck."},{"title":"🔒 Secure","details":"Customization of table-level, row-level, and property-level security are all built-in. Read More."}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}`),i={name:"index.md"};function l(s,c,d,p,m,u){const e=o("SiteFooter");return a(),r("div",null,[n(e)])}const g=t(i,[["render",l]]);export{_ as __pageData,g as default};
diff --git a/assets/index.md.J5qzQ5SM.lean.js b/assets/index.md.J5qzQ5SM.lean.js
new file mode 100644
index 000000000..a34f8d03a
--- /dev/null
+++ b/assets/index.md.J5qzQ5SM.lean.js
@@ -0,0 +1 @@
+import{_ as t,D as o,o as a,c as r,I as n}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse(`{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"Coalesce","text":"Accelerated Web App Development","tagline":"ASP.NET Core • EF Core • Vue.js • TypeScript","image":{"src":"/coalesce-icon-color.svg","alt":"Coalesce"},"actions":[{"theme":"brand","text":"Introduction","link":"/introduction"},{"theme":"alt","text":"Get Started","link":"/stacks/vue/getting-started"}]},"features":[{"title":"🖨 Code Generated","details":"Design your data model and build awesome pages. Coalesce generates the boring parts in the middle."},{"title":"🧩 Extensible","details":"All functionality in Coalesce is configurable or overridable. You'll never be boxed in or get stuck."},{"title":"🔒 Secure","details":"Customization of table-level, row-level, and property-level security are all built-in. Read More."}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}`),i={name:"index.md"};function l(s,c,d,p,m,u){const e=o("SiteFooter");return a(),r("div",null,[n(e)])}const g=t(i,[["render",l]]);export{_ as __pageData,g as default};
diff --git a/assets/inter-italic-cyrillic-ext.OVycGSDq.woff2 b/assets/inter-italic-cyrillic-ext.OVycGSDq.woff2
new file mode 100644
index 000000000..2a6872967
Binary files /dev/null and b/assets/inter-italic-cyrillic-ext.OVycGSDq.woff2 differ
diff --git a/assets/inter-italic-cyrillic.-nLMcIwj.woff2 b/assets/inter-italic-cyrillic.-nLMcIwj.woff2
new file mode 100644
index 000000000..f64035158
Binary files /dev/null and b/assets/inter-italic-cyrillic.-nLMcIwj.woff2 differ
diff --git a/assets/inter-italic-greek-ext.hznxWNZO.woff2 b/assets/inter-italic-greek-ext.hznxWNZO.woff2
new file mode 100644
index 000000000..002189603
Binary files /dev/null and b/assets/inter-italic-greek-ext.hznxWNZO.woff2 differ
diff --git a/assets/inter-italic-greek.PSfer2Kc.woff2 b/assets/inter-italic-greek.PSfer2Kc.woff2
new file mode 100644
index 000000000..71c265f85
Binary files /dev/null and b/assets/inter-italic-greek.PSfer2Kc.woff2 differ
diff --git a/assets/inter-italic-latin-ext.RnFly65-.woff2 b/assets/inter-italic-latin-ext.RnFly65-.woff2
new file mode 100644
index 000000000..9c1b9440e
Binary files /dev/null and b/assets/inter-italic-latin-ext.RnFly65-.woff2 differ
diff --git a/assets/inter-italic-latin.27E69YJn.woff2 b/assets/inter-italic-latin.27E69YJn.woff2
new file mode 100644
index 000000000..01fcf2072
Binary files /dev/null and b/assets/inter-italic-latin.27E69YJn.woff2 differ
diff --git a/assets/inter-italic-vietnamese.xzQHe1q1.woff2 b/assets/inter-italic-vietnamese.xzQHe1q1.woff2
new file mode 100644
index 000000000..e4f788ee0
Binary files /dev/null and b/assets/inter-italic-vietnamese.xzQHe1q1.woff2 differ
diff --git a/assets/inter-roman-cyrillic-ext.8T9wMG5w.woff2 b/assets/inter-roman-cyrillic-ext.8T9wMG5w.woff2
new file mode 100644
index 000000000..28593ccb8
Binary files /dev/null and b/assets/inter-roman-cyrillic-ext.8T9wMG5w.woff2 differ
diff --git a/assets/inter-roman-cyrillic.jIZ9REo5.woff2 b/assets/inter-roman-cyrillic.jIZ9REo5.woff2
new file mode 100644
index 000000000..a20adc161
Binary files /dev/null and b/assets/inter-roman-cyrillic.jIZ9REo5.woff2 differ
diff --git a/assets/inter-roman-greek-ext.9JiNzaSO.woff2 b/assets/inter-roman-greek-ext.9JiNzaSO.woff2
new file mode 100644
index 000000000..e3b0be76d
Binary files /dev/null and b/assets/inter-roman-greek-ext.9JiNzaSO.woff2 differ
diff --git a/assets/inter-roman-greek.Cb5wWeGA.woff2 b/assets/inter-roman-greek.Cb5wWeGA.woff2
new file mode 100644
index 000000000..f790e047d
Binary files /dev/null and b/assets/inter-roman-greek.Cb5wWeGA.woff2 differ
diff --git a/assets/inter-roman-latin-ext.GZWE-KO4.woff2 b/assets/inter-roman-latin-ext.GZWE-KO4.woff2
new file mode 100644
index 000000000..715bd903b
Binary files /dev/null and b/assets/inter-roman-latin-ext.GZWE-KO4.woff2 differ
diff --git a/assets/inter-roman-latin.bvIUbFQP.woff2 b/assets/inter-roman-latin.bvIUbFQP.woff2
new file mode 100644
index 000000000..a540b7afe
Binary files /dev/null and b/assets/inter-roman-latin.bvIUbFQP.woff2 differ
diff --git a/assets/inter-roman-vietnamese.paY3CzEB.woff2 b/assets/inter-roman-vietnamese.paY3CzEB.woff2
new file mode 100644
index 000000000..5a9f9cb9c
Binary files /dev/null and b/assets/inter-roman-vietnamese.paY3CzEB.woff2 differ
diff --git a/assets/introduction.md.aI2TP5X5.js b/assets/introduction.md.aI2TP5X5.js
new file mode 100644
index 000000000..331e173da
--- /dev/null
+++ b/assets/introduction.md.aI2TP5X5.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as o,R as a,a3 as i,a4 as r,a5 as n,a6 as s,a7 as l,a8 as d}from"./chunks/framework.g9eZ-ZSs.js";const v=JSON.parse('{"title":"Introduction","description":"Documentation home page for IntelliTect.Coalesce","frontmatter":{"lang":"en-US","title":"Introduction","description":"Documentation home page for IntelliTect.Coalesce"},"headers":[],"relativePath":"introduction.md","filePath":"introduction.md"}'),c={name:"introduction.md"},p=a('
Coalesce
Designed to help you quickly build amazing web applications, Coalesce is a rapid-development, code generation-based web application framework created by IntelliTect and built on top of:
C#, .NET, and ASP.NET Core are the backend foundation of all Coalesce applications.
Design your relational data model with Entity Framework. Coalesce will use that data model to generate an extensible, customizable CRUD API that will drive both your custom pages and the out-of-the-box admin pages.
Frontend code for Coalesce apps is generated for and written in TypeScript, enabling discovery of Coalesce features through Intellisense and providing confidence that your code won't break as your application grows.
Build an awesome frontend application with Vue.js. Coalesce will generate TypeScript ViewModels to facilitate rapid development of custom pages.
Vite is the development and build tooling for your frontend Vue code, enabling lightning-fast single-page application development. Coalesce integrates ASP.NET Core and Vite together, streamlining local development to require nothing more than a dotnet run or a single-click launch in your IDE.
Coalesce builds the part of your application that are mundane and monotonous to build:
Client side TypeScript ViewModels that mirror your data model for both lists and individual objects. Utilize these to rapidly build out your application's various pages.
APIs to interact with your models via endpoints like List, Get, Save, and more.
Out-of-the-box Vue Components for common controls like dates, selecting objects via drop downs, enums, etc. Dropdowns support searching and paging automatically.
A complete set of admin pages are provided, allowing you to read, create, edit, and delete data straight away without writing any additional code.
While Knockout.js is still supported by Coalesce, it is a deprecated option and not recommended for new projects. If you do still want to choose Knockout, click here.
',12),h=[p];function u(m,g,f,_,w,b){return t(),o("div",null,h)}const C=e(c,[["render",u]]);export{v as __pageData,C as default};
diff --git a/assets/introduction.md.aI2TP5X5.lean.js b/assets/introduction.md.aI2TP5X5.lean.js
new file mode 100644
index 000000000..1578d406f
--- /dev/null
+++ b/assets/introduction.md.aI2TP5X5.lean.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as o,R as a,a3 as i,a4 as r,a5 as n,a6 as s,a7 as l,a8 as d}from"./chunks/framework.g9eZ-ZSs.js";const v=JSON.parse('{"title":"Introduction","description":"Documentation home page for IntelliTect.Coalesce","frontmatter":{"lang":"en-US","title":"Introduction","description":"Documentation home page for IntelliTect.Coalesce"},"headers":[],"relativePath":"introduction.md","filePath":"introduction.md"}'),c={name:"introduction.md"},p=a("",12),h=[p];function u(m,g,f,_,w,b){return t(),o("div",null,h)}const C=e(c,[["render",u]]);export{v as __pageData,C as default};
diff --git a/assets/modeling_model-components_attributes.md.IUIXuOcX.js b/assets/modeling_model-components_attributes.md.IUIXuOcX.js
new file mode 100644
index 000000000..5875a7987
--- /dev/null
+++ b/assets/modeling_model-components_attributes.md.IUIXuOcX.js
@@ -0,0 +1 @@
+import{_ as e,o as a,c as t,R as o}from"./chunks/framework.g9eZ-ZSs.js";const b=JSON.parse('{"title":"Attributes","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes.md","filePath":"modeling/model-components/attributes.md"}'),r={name:"modeling/model-components/attributes.md"},i=o('
Coalesce provides a number of C# attributes that can be used to decorate your model classes and their properties in order to customize behavior, appearance, security, and more. Coalesce also supports a number of annotations from System.ComponentModel.DataAnnotations.
The displayed name and description of a property, as well as the order in which it appears in generated views, can be set via the [Display] attribute. By default, properties will be displayed in the order in which they are defined in their class.
Some values of DataType when provided to DataTypeAttribute on a string property will alter the behavior of the Vue Components. See c-display and See c-display for details.
Normally, Coalesce figures out which properties are foreign keys, but if you don't use standard EF naming conventions then you'll need to annotate with [ForeignKey] to help out both EF and Coalesce. See the Entity Framework Relationships documentation for more.
Sometimes, Coalesce (and EF, too) can have trouble figuring out what the foreign key is supposed to be for a collection navigation property. See the Entity Framework Relationships documentation for details on how and why to use [InverseProperty].
Primary Keys with [DatabaseGenerated(DatabaseGeneratedOption.None)] will be settable on the client and will be appropriately handled by the Standard Behaviors on the server. Unsupported on the Knockout front-end stack.
Model properties that aren't mapped to the database should be marked with [NotMapped] so that Coalesce doesn't try to load them from the database when searching or carrying out the Default Loading Behavior.
Properties with [DefaultValue] will receive the specified value when a new ViewModel is instantiated on the client. This enables scenarios like pre-filling a required property with a suggested value.
',30),n=[i];function s(l,d,h,c,p,u){return a(),t("div",null,n)}const f=e(r,[["render",s]]);export{b as __pageData,f as default};
diff --git a/assets/modeling_model-components_attributes.md.IUIXuOcX.lean.js b/assets/modeling_model-components_attributes.md.IUIXuOcX.lean.js
new file mode 100644
index 000000000..d624eedff
--- /dev/null
+++ b/assets/modeling_model-components_attributes.md.IUIXuOcX.lean.js
@@ -0,0 +1 @@
+import{_ as e,o as a,c as t,R as o}from"./chunks/framework.g9eZ-ZSs.js";const b=JSON.parse('{"title":"Attributes","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes.md","filePath":"modeling/model-components/attributes.md"}'),r={name:"modeling/model-components/attributes.md"},i=o("",30),n=[i];function s(l,d,h,c,p,u){return a(),t("div",null,n)}const f=e(r,[["render",s]]);export{b as __pageData,f as default};
diff --git a/assets/modeling_model-components_attributes_client-validation.md.1GRmSFWa.js b/assets/modeling_model-components_attributes_client-validation.md.1GRmSFWa.js
new file mode 100644
index 000000000..16741ae50
--- /dev/null
+++ b/assets/modeling_model-components_attributes_client-validation.md.1GRmSFWa.js
@@ -0,0 +1,31 @@
+import{_ as r,D as a,o as c,c as D,I as o,w as t,R as n,k as s,a as l}from"./chunks/framework.g9eZ-ZSs.js";const S=JSON.parse('{"title":"[ClientValidation]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/client-validation.md","filePath":"modeling/model-components/attributes/client-validation.md"}'),i={name:"modeling/model-components/attributes/client-validation.md"},y=n(`
The [IntelliTect.Coalesce.DataAnnotations.ClientValidation] attribute is used to control the behavior of client-side model validation and to add additional client-only validation parameters. Database validation is available via standard System.ComponentModel.DataAnnotations annotations.
These propagate to the client as validations in TypeScript via generated Metadata and ViewModel rules (for Vue) or Knockout-Validation rules (for Knockout). For both stacks, any failing validation rules prevent saves from going to the server.
WARNING
This attribute controls client-side validation only. To perform server-side validation, create a custom Behaviors class for your types and/or place C# validation attributes on your models. Read More.
If set to true, any client validation errors on the property will not prevent saving on the client. This includes all client-side validation, including null-checking for required foreign keys and other validations that are implicit. This also includes other explicit validation from System.ComponentModel.DataAnnotations annotations.
Instead, validation errors will be treated only as warnings, and will be available through the warnings: KnockoutValidationErrors property on the TypeScript ViewModel.
Note
Use AllowSave = true to allow partially complete data to still be saved, protecting your user from data loss upon navigation while still hinting to them that they are not done filling out data.
',3),d=s("p",null,"Set an error message to be used if any client validations fail",-1),u=s("h3",{id:"validation-rule-properties",tabindex:"-1"},[l("Validation Rule Properties "),s("a",{class:"header-anchor",href:"#validation-rule-properties","aria-label":'Permalink to "Validation Rule Properties"'},"")],-1),h=s("p",null,[l("In addition to the following properties, you also customize validation on a per-instance basis of the "),s("a",{href:"/Coalesce/stacks/vue/layers/viewmodels.html#viewmodels"},"ViewModels"),l(" using the "),s("a",{href:"/Coalesce/stacks/vue/layers/viewmodels.html#rules-validation"},"Rules/Validation"),l(" methods.")],-1),b=s("div",{class:"language-c#"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"c#"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsRequired"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MinValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MaxValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MinLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MaxLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," Pattern"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsEmail"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsPhoneUs"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")])])])],-1),E=s("p",null,[l("The following attribute properties all map directly to "),s("a",{href:"https://github.com/Knockout-Contrib/Knockout-Validation/",target:"_blank",rel:"noreferrer"},"Knockout-Validation"),l(" properties.")],-1),g=s("div",{class:"language-c#"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"c#"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsRequired"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MinValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MaxValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MinLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MaxLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," Step"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," Pattern"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsEmail"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsPhoneUs"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsDate"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsDateIso"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsNumber"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsDigit"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")])])])],-1),F=s("p",null,"The following attribute properties are outputted to TypeScript unquoted. If you need to assert equality to a string, wrap the value you set to this property in quotes. Other literals (numerics, booleans, etc) need no wrapping.",-1),m=s("div",{class:"language-c#"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"c#"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," Equal"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," NotEqual"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")])])])],-1),v=s("p",null,[l("The following two properties may be used together to specify a custom "),s("a",{href:"https://github.com/Knockout-Contrib/Knockout-Validation/",target:"_blank",rel:"noreferrer"},"Knockout-Validation"),l(" property.")],-1),_=s("p",null,[l("It will be emitted into the TypeScript as "),s("code",null,"this.extend({ CustomName: CustomValue })"),l(". Neither value will be quoted in the emitted TypeScript - add quotes to your value as needed to generate valid TypeScript.")],-1),f=s("div",{class:"language-c#"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"c#"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," CustomName"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," CustomValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")])])])],-1);function k(V,T,w,I,M,x){const e=a("Prop"),p=a("CodeTabs");return c(),D("div",null,[y,o(e,{def:"public bool AllowSave { get; set; } // Knockout Only"}),C,o(e,{def:"public string ErrorMessage { get; set; }"}),d,u,o(p,null,{vue:t(()=>[h,b]),knockout:t(()=>[E,g,F,m,v,_,f]),_:1})])}const q=r(i,[["render",k]]);export{S as __pageData,q as default};
diff --git a/assets/modeling_model-components_attributes_client-validation.md.1GRmSFWa.lean.js b/assets/modeling_model-components_attributes_client-validation.md.1GRmSFWa.lean.js
new file mode 100644
index 000000000..7e7bf1414
--- /dev/null
+++ b/assets/modeling_model-components_attributes_client-validation.md.1GRmSFWa.lean.js
@@ -0,0 +1,22 @@
+import{_ as r,D as a,o as c,c as D,I as o,w as t,R as n,k as s,a as l}from"./chunks/framework.g9eZ-ZSs.js";const S=JSON.parse('{"title":"[ClientValidation]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/client-validation.md","filePath":"modeling/model-components/attributes/client-validation.md"}'),i={name:"modeling/model-components/attributes/client-validation.md"},y=n("",8),C=n("",3),d=s("p",null,"Set an error message to be used if any client validations fail",-1),u=s("h3",{id:"validation-rule-properties",tabindex:"-1"},[l("Validation Rule Properties "),s("a",{class:"header-anchor",href:"#validation-rule-properties","aria-label":'Permalink to "Validation Rule Properties"'},"")],-1),h=s("p",null,[l("In addition to the following properties, you also customize validation on a per-instance basis of the "),s("a",{href:"/Coalesce/stacks/vue/layers/viewmodels.html#viewmodels"},"ViewModels"),l(" using the "),s("a",{href:"/Coalesce/stacks/vue/layers/viewmodels.html#rules-validation"},"Rules/Validation"),l(" methods.")],-1),b=s("div",{class:"language-c#"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"c#"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsRequired"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MinValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MaxValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MinLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MaxLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," Pattern"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsEmail"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsPhoneUs"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")])])])],-1),E=s("p",null,[l("The following attribute properties all map directly to "),s("a",{href:"https://github.com/Knockout-Contrib/Knockout-Validation/",target:"_blank",rel:"noreferrer"},"Knockout-Validation"),l(" properties.")],-1),g=s("div",{class:"language-c#"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"c#"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsRequired"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MinValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MaxValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MinLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," MaxLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#569CD6"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," double"),s("span",{style:{color:"#9CDCFE"}}," Step"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," Pattern"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsEmail"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsPhoneUs"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsDate"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsDateIso"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsNumber"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," bool"),s("span",{style:{color:"#9CDCFE"}}," IsDigit"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")])])])],-1),F=s("p",null,"The following attribute properties are outputted to TypeScript unquoted. If you need to assert equality to a string, wrap the value you set to this property in quotes. Other literals (numerics, booleans, etc) need no wrapping.",-1),m=s("div",{class:"language-c#"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"c#"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," Equal"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," NotEqual"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")])])])],-1),v=s("p",null,[l("The following two properties may be used together to specify a custom "),s("a",{href:"https://github.com/Knockout-Contrib/Knockout-Validation/",target:"_blank",rel:"noreferrer"},"Knockout-Validation"),l(" property.")],-1),_=s("p",null,[l("It will be emitted into the TypeScript as "),s("code",null,"this.extend({ CustomName: CustomValue })"),l(". Neither value will be quoted in the emitted TypeScript - add quotes to your value as needed to generate valid TypeScript.")],-1),f=s("div",{class:"language-c#"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"c#"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," CustomName"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#569CD6"}}," string"),s("span",{style:{color:"#9CDCFE"}}," CustomValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")])])])],-1);function k(V,T,w,I,M,x){const e=a("Prop"),p=a("CodeTabs");return c(),D("div",null,[y,o(e,{def:"public bool AllowSave { get; set; } // Knockout Only"}),C,o(e,{def:"public string ErrorMessage { get; set; }"}),d,u,o(p,null,{vue:t(()=>[h,b]),knockout:t(()=>[E,g,F,m,v,_,f]),_:1})])}const q=r(i,[["render",k]]);export{S as __pageData,q as default};
diff --git a/assets/modeling_model-components_attributes_coalesce.md.Lkb6B2Rs.js b/assets/modeling_model-components_attributes_coalesce.md.Lkb6B2Rs.js
new file mode 100644
index 000000000..5b0575e1d
--- /dev/null
+++ b/assets/modeling_model-components_attributes_coalesce.md.Lkb6B2Rs.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as o,R as a}from"./chunks/framework.g9eZ-ZSs.js";const u=JSON.parse('{"title":"[Coalesce]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/coalesce.md","filePath":"modeling/model-components/attributes/coalesce.md"}'),s={name:"modeling/model-components/attributes/coalesce.md"},l=a('
Used to mark a type or member for generation by Coalesce.
Some types and members will be implicitly included in generation - for example, all types represented by a DbSet<T> on a DbContext that has a [Coalesce] attribute will automatically be included. Properties on these types will also be generated for unless explicitly excluded, since this is by far the most common usage scenario in Coalesce.
On the other hand, Methods on these types will not have endpoints generated unless they are explicitly annotated with [Coalesce] to avoid accidentally exposing methods that were perhaps not intended to be exposed.
The documentation pages for types and members that require/accept this attribute will state as such. An exhaustive list of all valid targets for [Coalesce] will not be found on this page.
',5),n=[l];function c(i,d,r,p,m,h){return t(),o("div",null,n)}const b=e(s,[["render",c]]);export{u as __pageData,b as default};
diff --git a/assets/modeling_model-components_attributes_coalesce.md.Lkb6B2Rs.lean.js b/assets/modeling_model-components_attributes_coalesce.md.Lkb6B2Rs.lean.js
new file mode 100644
index 000000000..d7e738c40
--- /dev/null
+++ b/assets/modeling_model-components_attributes_coalesce.md.Lkb6B2Rs.lean.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as o,R as a}from"./chunks/framework.g9eZ-ZSs.js";const u=JSON.parse('{"title":"[Coalesce]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/coalesce.md","filePath":"modeling/model-components/attributes/coalesce.md"}'),s={name:"modeling/model-components/attributes/coalesce.md"},l=a("",5),n=[l];function c(i,d,r,p,m,h){return t(),o("div",null,n)}const b=e(s,[["render",c]]);export{u as __pageData,b as default};
diff --git a/assets/modeling_model-components_attributes_controller-action.md.Ldygq9AT.js b/assets/modeling_model-components_attributes_controller-action.md.Ldygq9AT.js
new file mode 100644
index 000000000..13b1b3a99
--- /dev/null
+++ b/assets/modeling_model-components_attributes_controller-action.md.Ldygq9AT.js
@@ -0,0 +1,27 @@
+import{_ as o,D as e,o as l,c as p,I as a,R as s}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"[ControllerAction]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/controller-action.md","filePath":"modeling/model-components/attributes/controller-action.md"}'),t={name:"modeling/model-components/attributes/controller-action.md"},r=s(`
The HTTP method to use on the generated API Controller.
Enum values are:
HttpMethod.Post Use the POST method.
HttpMethod.Get Use the GET method.
HttpMethod.Put Use the PUT method.
HttpMethod.Delete Use the DELETE method.
HttpMethod.Patch Use the PATCH method.
",3),D=s('
For HTTP GET model instance methods, if VaryByProperty is set to the name of a property on the parent model class, ETag headers based on the value of this property will be used to implement caching. If the client provides a matching If-None-Match Header with the request, the method will not be invoked and HTTP Status `304 Not Modified`` will be returned.
Additionally, if the VaryByProperty is set to a client-exposed property, the value of the property will be included in the query string when performing API calls to invoke the method. If the query string value matches the current value on the model, a long-term Cache-Control header will be set on the response, allowing the client to avoid making future invocations to the same method while the value of the VaryByProperty remains the same.
',2);function y(i,C,d,h,m,u){const n=e("Prop");return l(),p("div",null,[r,a(n,{def:"public HttpMethod Method { get; set; } = HttpMethod.Post;",ctor:"1"}),c,a(n,{def:"public string VaryByProperty { get; set; }"}),D])}const P=o(t,[["render",y]]);export{_ as __pageData,P as default};
diff --git a/assets/modeling_model-components_attributes_controller-action.md.Ldygq9AT.lean.js b/assets/modeling_model-components_attributes_controller-action.md.Ldygq9AT.lean.js
new file mode 100644
index 000000000..fb18a775c
--- /dev/null
+++ b/assets/modeling_model-components_attributes_controller-action.md.Ldygq9AT.lean.js
@@ -0,0 +1 @@
+import{_ as o,D as e,o as l,c as p,I as a,R as s}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"[ControllerAction]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/controller-action.md","filePath":"modeling/model-components/attributes/controller-action.md"}'),t={name:"modeling/model-components/attributes/controller-action.md"},r=s("",5),c=s("",3),D=s("",2);function y(i,C,d,h,m,u){const n=e("Prop");return l(),p("div",null,[r,a(n,{def:"public HttpMethod Method { get; set; } = HttpMethod.Post;",ctor:"1"}),c,a(n,{def:"public string VaryByProperty { get; set; }"}),D])}const P=o(t,[["render",y]]);export{_ as __pageData,P as default};
diff --git a/assets/modeling_model-components_attributes_controller.md.4MsqYLok.js b/assets/modeling_model-components_attributes_controller.md.4MsqYLok.js
new file mode 100644
index 000000000..889ce1b2e
--- /dev/null
+++ b/assets/modeling_model-components_attributes_controller.md.4MsqYLok.js
@@ -0,0 +1,7 @@
+import{_ as n,D as a,o as r,c,I as t,R as s,k as e,a as l}from"./chunks/framework.g9eZ-ZSs.js";const I=JSON.parse('{"title":"[Controller]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/controller.md","filePath":"modeling/model-components/attributes/controller.md"}'),p={name:"modeling/model-components/attributes/controller.md"},i=s(`
Determines whether or not a [Route] annotation will be placed on the generated API controller. Set to false to prevent emission of the [Route] attribute.
Use cases include:
Defining your routes through IRouteBuilder in Startup.cs instead
Preventing API controllers from being exposed by default.
Routing to your own custom controller that inherits from the generated API controller in order to implement more granular or complex authorization logic.
",3),u=e("p",null,"If set, will determine the name of the generated API controller.",-1),h=e("p",null,[l("Takes precedence over the value of "),e("code",null,"ApiControllerSuffix"),l(".")],-1),m=e("p",null,"If set, will be appended to the default name of the API controller generated for this model.",-1),D=e("p",null,[l("Will be overridden by the value of "),e("code",null,"ApiControllerName"),l(" if it is set.")],-1),_=s('
If true, actions on the generated API controller will have an access modifier of protected instead of public.
In order to consume the generated API controller, you must inherit from the generated controller and override each desired generated action method via hiding (i.e. use public new ..., not public override ...).
Note
If you inherit from the generated API controllers and override their methods without setting ApiActionsProtected = true, all non-overridden actions from the generated controller will still be exposed as normal.
',3);function f(y,C,g,b,A,P){const o=a("Prop");return r(),c("div",null,[i,t(o,{def:"public bool ApiRouted { get; set; } = true;"}),d,t(o,{def:"public string ApiControllerName { get; set; } = null;"}),u,h,t(o,{def:"public string ApiControllerSuffix { get; set; } = null;"}),m,D,t(o,{def:"public bool ApiActionsProtected { get; set; } = false;"}),_])}const T=n(p,[["render",f]]);export{I as __pageData,T as default};
diff --git a/assets/modeling_model-components_attributes_controller.md.4MsqYLok.lean.js b/assets/modeling_model-components_attributes_controller.md.4MsqYLok.lean.js
new file mode 100644
index 000000000..736261d86
--- /dev/null
+++ b/assets/modeling_model-components_attributes_controller.md.4MsqYLok.lean.js
@@ -0,0 +1 @@
+import{_ as n,D as a,o as r,c,I as t,R as s,k as e,a as l}from"./chunks/framework.g9eZ-ZSs.js";const I=JSON.parse('{"title":"[Controller]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/controller.md","filePath":"modeling/model-components/attributes/controller.md"}'),p={name:"modeling/model-components/attributes/controller.md"},i=s("",7),d=s("",3),u=e("p",null,"If set, will determine the name of the generated API controller.",-1),h=e("p",null,[l("Takes precedence over the value of "),e("code",null,"ApiControllerSuffix"),l(".")],-1),m=e("p",null,"If set, will be appended to the default name of the API controller generated for this model.",-1),D=e("p",null,[l("Will be overridden by the value of "),e("code",null,"ApiControllerName"),l(" if it is set.")],-1),_=s("",3);function f(y,C,g,b,A,P){const o=a("Prop");return r(),c("div",null,[i,t(o,{def:"public bool ApiRouted { get; set; } = true;"}),d,t(o,{def:"public string ApiControllerName { get; set; } = null;"}),u,h,t(o,{def:"public string ApiControllerSuffix { get; set; } = null;"}),m,D,t(o,{def:"public bool ApiActionsProtected { get; set; } = false;"}),_])}const T=n(p,[["render",f]]);export{I as __pageData,T as default};
diff --git a/assets/modeling_model-components_attributes_create-controller.md.R_UFOdqz.js b/assets/modeling_model-components_attributes_create-controller.md.R_UFOdqz.js
new file mode 100644
index 000000000..4358593f3
--- /dev/null
+++ b/assets/modeling_model-components_attributes_create-controller.md.R_UFOdqz.js
@@ -0,0 +1,7 @@
+import{_ as a,D as o,o as l,c as n,I as s,R as t}from"./chunks/framework.g9eZ-ZSs.js";const m=JSON.parse('{"title":"[CreateController]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/create-controller.md","filePath":"modeling/model-components/attributes/create-controller.md"}'),r={name:"modeling/model-components/attributes/create-controller.md"},p=t(`
public class Person
+{
+ public int PersonId { get; set; }
+
+ [DateType(DateTypeAttribute.DateTypes.DateOnly)]
+ public DateTimeOffset? BirthDate { get; set; }
+}
`,5),i=e("p",null,"The type of date the property represents.",-1),y=e("p",null,"Enum values are:",-1),d=e("ul",null,[e("li",null,[e("code",null,"DateTypeAttribute.DateTypes.DateTime"),a(" Subject is both a date and time.")]),e("li",null,[e("code",null,"DateTypeAttribute.DateTypes.DateOnly"),a(" Subject is only a date with no significant time component.")])],-1);function u(m,_,h,C,T,b){const s=n("Prop");return o(),p("div",null,[D,l(s,{def:"public DateTypes DateType { get; set; } = DateTypes.DateTime; ",ctor:"1"}),a(),i,y,d])}const E=t(c,[["render",u]]);export{g as __pageData,E as default};
diff --git a/assets/modeling_model-components_attributes_date-type.md.IriIMwEk.lean.js b/assets/modeling_model-components_attributes_date-type.md.IriIMwEk.lean.js
new file mode 100644
index 000000000..c585e6a22
--- /dev/null
+++ b/assets/modeling_model-components_attributes_date-type.md.IriIMwEk.lean.js
@@ -0,0 +1 @@
+import{_ as t,D as n,o,c as p,I as l,a,R as r,k as e}from"./chunks/framework.g9eZ-ZSs.js";const g=JSON.parse('{"title":"[DateType]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/date-type.md","filePath":"modeling/model-components/attributes/date-type.md"}'),c={name:"modeling/model-components/attributes/date-type.md"},D=r("",5),i=e("p",null,"The type of date the property represents.",-1),y=e("p",null,"Enum values are:",-1),d=e("ul",null,[e("li",null,[e("code",null,"DateTypeAttribute.DateTypes.DateTime"),a(" Subject is both a date and time.")]),e("li",null,[e("code",null,"DateTypeAttribute.DateTypes.DateOnly"),a(" Subject is only a date with no significant time component.")])],-1);function u(m,_,h,C,T,b){const s=n("Prop");return o(),p("div",null,[D,l(s,{def:"public DateTypes DateType { get; set; } = DateTypes.DateTime; ",ctor:"1"}),a(),i,y,d])}const E=t(c,[["render",u]]);export{g as __pageData,E as default};
diff --git a/assets/modeling_model-components_attributes_default-order-by.md.2t7Z9gZk.js b/assets/modeling_model-components_attributes_default-order-by.md.2t7Z9gZk.js
new file mode 100644
index 000000000..83d4bcc98
--- /dev/null
+++ b/assets/modeling_model-components_attributes_default-order-by.md.2t7Z9gZk.js
@@ -0,0 +1,23 @@
+import{_ as l,D as o,o as p,c as t,I as n,a as e,R as r,k as s}from"./chunks/framework.g9eZ-ZSs.js";const O=JSON.parse('{"title":"[DefaultOrderBy]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/default-order-by.md","filePath":"modeling/model-components/attributes/default-order-by.md"}'),c={name:"modeling/model-components/attributes/default-order-by.md"},D=r(`
Allows setting of the default manner in which the data returned to the client will be sorted. Multiple fields can be used to sort an object by specifying an index.
This affects the sort order both when requesting a list of the model itself, as well as when the model appears as a child collection off of a navigation property of another object.
In the first case (a list of the model itself), this can be overridden by setting the orderBy or orderByDescending property on the TypeScript ListViewModel - see TypeScript List ViewModels.
public class Person
+{
+ public int PersonId { get; set; }
+
+ public int DepartmentId { get; set; }
+
+ [DefaultOrderBy(FieldOrder = 0, FieldName = nameof(Department.Order))]
+ public Department Department { get; set; }
+
+ [DefaultOrderBy(FieldOrder = 1)]
+ public string LastName { get; set; }
+}
c#
public class Person
+{
+ public int PersonId { get; set; }
+
+ public int DepartmentId { get; set; }
+
+ [DefaultOrderBy(FieldOrder = 0, FieldName = nameof(Department.Order))]
+ public Department Department { get; set; }
+
+ [DefaultOrderBy(FieldOrder = 1)]
+ public string LastName { get; set; }
+}
`,8),i=s("p",null,"Specify the index of this field when sorting by multiple fields.",-1),y=s("p",null,[e("Lower-valued properties will be used first; higher-valued properties will be used as a tiebreaker (i.e. "),s("code",null,".ThenBy(...)"),e(").")],-1),d=s("p",null,"Specify the direction of the ordering for the property.",-1),C=s("p",null,"Enum values are:",-1),u=s("ul",null,[s("li",null,[s("code",null,"DefaultOrderByAttribute.OrderByDirections.Ascending")]),s("li",null,[s("code",null,"DefaultOrderByAttribute.OrderByDirections.Descending")])],-1),h=s("p",null,[e("When using the "),s("code",null,"DefaultOrderByAttribute"),e(" on an object property, specifies the field on the object to use for sorting. See the first example above.")],-1);function f(m,b,g,_,E,B){const a=o("Prop");return p(),t("div",null,[D,n(a,{def:"public int FieldOrder { get; set; } = 0; ",ctor:"1"}),e(),i,y,n(a,{def:"public OrderByDirections OrderByDirection { get; set; } = OrderByDirections.Ascending;",ctor:"2"}),d,C,u,n(a,{def:"public string FieldName { get; set; }"}),h])}const v=l(c,[["render",f]]);export{O as __pageData,v as default};
diff --git a/assets/modeling_model-components_attributes_default-order-by.md.2t7Z9gZk.lean.js b/assets/modeling_model-components_attributes_default-order-by.md.2t7Z9gZk.lean.js
new file mode 100644
index 000000000..32211e9e8
--- /dev/null
+++ b/assets/modeling_model-components_attributes_default-order-by.md.2t7Z9gZk.lean.js
@@ -0,0 +1 @@
+import{_ as l,D as o,o as p,c as t,I as n,a as e,R as r,k as s}from"./chunks/framework.g9eZ-ZSs.js";const O=JSON.parse('{"title":"[DefaultOrderBy]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/default-order-by.md","filePath":"modeling/model-components/attributes/default-order-by.md"}'),c={name:"modeling/model-components/attributes/default-order-by.md"},D=r("",8),i=s("p",null,"Specify the index of this field when sorting by multiple fields.",-1),y=s("p",null,[e("Lower-valued properties will be used first; higher-valued properties will be used as a tiebreaker (i.e. "),s("code",null,".ThenBy(...)"),e(").")],-1),d=s("p",null,"Specify the direction of the ordering for the property.",-1),C=s("p",null,"Enum values are:",-1),u=s("ul",null,[s("li",null,[s("code",null,"DefaultOrderByAttribute.OrderByDirections.Ascending")]),s("li",null,[s("code",null,"DefaultOrderByAttribute.OrderByDirections.Descending")])],-1),h=s("p",null,[e("When using the "),s("code",null,"DefaultOrderByAttribute"),e(" on an object property, specifies the field on the object to use for sorting. See the first example above.")],-1);function f(m,b,g,_,E,B){const a=o("Prop");return p(),t("div",null,[D,n(a,{def:"public int FieldOrder { get; set; } = 0; ",ctor:"1"}),e(),i,y,n(a,{def:"public OrderByDirections OrderByDirection { get; set; } = OrderByDirections.Ascending;",ctor:"2"}),d,C,u,n(a,{def:"public string FieldName { get; set; }"}),h])}const v=l(c,[["render",f]]);export{O as __pageData,v as default};
diff --git a/assets/modeling_model-components_attributes_dto-includes-excludes.md.gWiALlZf.js b/assets/modeling_model-components_attributes_dto-includes-excludes.md.gWiALlZf.js
new file mode 100644
index 000000000..da534569d
--- /dev/null
+++ b/assets/modeling_model-components_attributes_dto-includes-excludes.md.gWiALlZf.js
@@ -0,0 +1,40 @@
+import{_ as c,D as o,o as r,c as D,I as l,w as n,a as e,R as a,k as s}from"./chunks/framework.g9eZ-ZSs.js";const F=JSON.parse('{"title":"[DtoIncludes] & [DtoExcludes]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/dto-includes-excludes.md","filePath":"modeling/model-components/attributes/dto-includes-excludes.md"}'),i={name:"modeling/model-components/attributes/dto-includes-excludes.md"},d=a(`
Allows for easily controlling what data gets set to the client. When requesting data from the generated client-side list view models, you can specify an includes property on the ViewModel or ListViewModel.
For more information about the includes string, see Includes String.
When the database entries are returned to the client they will be trimmed based on the requested includes string and the values in DtoExcludes and DtoIncludes.
DANGER
These attributes are not security attributes - consumers of your application's API can set the includes string to any value when making a request.
Do not use them to keep certain data private - use the Security Attributes family of attributes for that.
It is important to note that the value of the includes string will match against these attributes on any of your models that appears in the object graph being mapped to DTOs - it is not limited only to the model type of the root object.
Important
DtoIncludes does not ensure that specific data will be loaded from the database. It only permits what is already loaded into the current EF DbContext to be returned from the API. See Data Sources to learn how to control what data gets loaded from the database.
public class Person
+{
+ // Don't include CreatedBy when editing - will be included for all other views
+ [DtoExcludes("Editor")]
+ public AppUser CreatedBy { get; set; }
+
+ // Only include the Person's Department when \`includes == "details"\` on the TypeScript ViewModel.
+ [DtoIncludes("details")]
+ public Department Department { get; set; }
+
+ // LastName will be included in all views
+ public string LastName { get; set; }
+}
+
+public class Department
+{
+ [DtoIncludes("details")]
+ public ICollection<Person> People { get; set; }
+}
Client code:
`,11),y=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList.$items will not contain CreatedBy nor Department objects.")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList2.items will be allowed to contain both CreatedBy and Department objects. ")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Department will be allowed to include its other Person objects.")])])])],-1),C=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}}," // objects in personList.items will not contain CreatedBy nor Department objects.")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}}," // objects in personList2.items will be allowed to contain both CreatedBy and Department objects. Department will be allowed to include its other Person objects.")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")])])])],-1),u=s("h2",{id:"properties",tabindex:"-1"},[e("Properties "),s("a",{class:"header-anchor",href:"#properties","aria-label":'Permalink to "Properties"'},"")],-1),h=a('
A comma-delimited list of values of includes on which to operate.
For DtoIncludes, this will be the values of includes for which this property will be allowed to be serialized and sent to the client.
For DtoExcludes, this will be the values of includes for which this property will not be serialized and sent to the client.
',3);function m(b,E,w,g,_,f){const t=o("CodeTabs"),p=o("Prop");return r(),D("div",null,[d,l(t,null,{vue:n(()=>[y]),knockout:n(()=>[C]),_:1}),u,l(p,{def:"public string ContentViews { get; set; }",ctor:"1"}),e(),h])}const v=c(i,[["render",m]]);export{F as __pageData,v as default};
diff --git a/assets/modeling_model-components_attributes_dto-includes-excludes.md.gWiALlZf.lean.js b/assets/modeling_model-components_attributes_dto-includes-excludes.md.gWiALlZf.lean.js
new file mode 100644
index 000000000..080a34555
--- /dev/null
+++ b/assets/modeling_model-components_attributes_dto-includes-excludes.md.gWiALlZf.lean.js
@@ -0,0 +1,22 @@
+import{_ as c,D as o,o as r,c as D,I as l,w as n,a as e,R as a,k as s}from"./chunks/framework.g9eZ-ZSs.js";const F=JSON.parse('{"title":"[DtoIncludes] & [DtoExcludes]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/dto-includes-excludes.md","filePath":"modeling/model-components/attributes/dto-includes-excludes.md"}'),i={name:"modeling/model-components/attributes/dto-includes-excludes.md"},d=a("",11),y=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList.$items will not contain CreatedBy nor Department objects.")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList2.items will be allowed to contain both CreatedBy and Department objects. ")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Department will be allowed to include its other Person objects.")])])])],-1),C=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}}," // objects in personList.items will not contain CreatedBy nor Department objects.")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}}," // objects in personList2.items will be allowed to contain both CreatedBy and Department objects. Department will be allowed to include its other Person objects.")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")])])])],-1),u=s("h2",{id:"properties",tabindex:"-1"},[e("Properties "),s("a",{class:"header-anchor",href:"#properties","aria-label":'Permalink to "Properties"'},"")],-1),h=a("",3);function m(b,E,w,g,_,f){const t=o("CodeTabs"),p=o("Prop");return r(),D("div",null,[d,l(t,null,{vue:n(()=>[y]),knockout:n(()=>[C]),_:1}),u,l(p,{def:"public string ContentViews { get; set; }",ctor:"1"}),e(),h])}const v=c(i,[["render",m]]);export{F as __pageData,v as default};
diff --git a/assets/modeling_model-components_attributes_execute.md.OmkAEPLi.js b/assets/modeling_model-components_attributes_execute.md.OmkAEPLi.js
new file mode 100644
index 000000000..ae29345eb
--- /dev/null
+++ b/assets/modeling_model-components_attributes_execute.md.OmkAEPLi.js
@@ -0,0 +1,11 @@
+import{_ as l,D as n,o as r,c,I as o,R as t,k as e,a}from"./chunks/framework.g9eZ-ZSs.js";const P=JSON.parse('{"title":"[Execute]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/execute.md","filePath":"modeling/model-components/attributes/execute.md"}'),i={name:"modeling/model-components/attributes/execute.md"},p=t(`
`,6),d=e("p",null,"A comma-separated list of roles which are allowed to execute the method.",-1),u=t("
The level of access to allow for the action for the method.
Enum values are:
SecurityPermissionLevels.AllowAll Allow all users to perform the action for the attribute, including users who are not authenticated at all.
SecurityPermissionLevels.AllowAuthorized Allow only users who are members of the roles specified on the attribute to perform the action. If no roles are specified on the attribute, then all authenticated users are allowed (no anonymous access).
SecurityPermissionLevels.DenyAll Deny the action to all users, regardless of authentication status or authorization level. If DenyAll is used, no API endpoint for the action will be generated.
",3),h=e("p",null,"If true, the method's arguments will be cleared after a successful invocation on admin pages.",-1),D=e("p",null,[a("If non-null, overrides the value of "),e("a",{href:"/Coalesce/topics/security.html#attribute-validation"},[e("code",null,"CoalesceOptions.ValidateAttributesForMethods")]),a(" when determining whether to perform automatic server-side validation of the method's parameters.")],-1),m=e("p",null,"If validation is performed, the method's parameters will be validated by the server and the method invocation prevented if errors are found.",-1);function y(_,f,b,C,v,g){const s=n("Prop");return r(),c("div",null,[p,o(s,{def:"public string Roles { get; set; }"}),d,o(s,{def:"public SecurityPermissionLevels PermissionLevel { get; set; } = SecurityPermissionLevels.AllowAuthorized;"}),u,o(s,{def:"public bool AutoClear { get; set; }"}),h,o(s,{def:"public bool? ValidateAttributes { get; set; }"}),D,m])}const x=l(i,[["render",y]]);export{P as __pageData,x as default};
diff --git a/assets/modeling_model-components_attributes_execute.md.OmkAEPLi.lean.js b/assets/modeling_model-components_attributes_execute.md.OmkAEPLi.lean.js
new file mode 100644
index 000000000..ea7826a35
--- /dev/null
+++ b/assets/modeling_model-components_attributes_execute.md.OmkAEPLi.lean.js
@@ -0,0 +1 @@
+import{_ as l,D as n,o as r,c,I as o,R as t,k as e,a}from"./chunks/framework.g9eZ-ZSs.js";const P=JSON.parse('{"title":"[Execute]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/execute.md","filePath":"modeling/model-components/attributes/execute.md"}'),i={name:"modeling/model-components/attributes/execute.md"},p=t("",6),d=e("p",null,"A comma-separated list of roles which are allowed to execute the method.",-1),u=t("",3),h=e("p",null,"If true, the method's arguments will be cleared after a successful invocation on admin pages.",-1),D=e("p",null,[a("If non-null, overrides the value of "),e("a",{href:"/Coalesce/topics/security.html#attribute-validation"},[e("code",null,"CoalesceOptions.ValidateAttributesForMethods")]),a(" when determining whether to perform automatic server-side validation of the method's parameters.")],-1),m=e("p",null,"If validation is performed, the method's parameters will be validated by the server and the method invocation prevented if errors are found.",-1);function y(_,f,b,C,v,g){const s=n("Prop");return r(),c("div",null,[p,o(s,{def:"public string Roles { get; set; }"}),d,o(s,{def:"public SecurityPermissionLevels PermissionLevel { get; set; } = SecurityPermissionLevels.AllowAuthorized;"}),u,o(s,{def:"public bool AutoClear { get; set; }"}),h,o(s,{def:"public bool? ValidateAttributes { get; set; }"}),D,m])}const x=l(i,[["render",y]]);export{P as __pageData,x as default};
diff --git a/assets/modeling_model-components_attributes_hidden.md.MV6N1IoA.js b/assets/modeling_model-components_attributes_hidden.md.MV6N1IoA.js
new file mode 100644
index 000000000..5e6d51a19
--- /dev/null
+++ b/assets/modeling_model-components_attributes_hidden.md.MV6N1IoA.js
@@ -0,0 +1,7 @@
+import{_ as a,D as t,o as n,c as o,I as l,R as e}from"./chunks/framework.g9eZ-ZSs.js";const b=JSON.parse('{"title":"[Hidden]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/hidden.md","filePath":"modeling/model-components/attributes/hidden.md"}'),r={name:"modeling/model-components/attributes/hidden.md"},i=e(`
HiddenAttribute.Areas.None Hide from no generated views. Primary and Foreign keys are hidden by default - setting this value explicitly can override this default behavior.
HiddenAttribute.Areas.All Hide from all generated views
HiddenAttribute.Areas.List Hide from generated list views only (Knockout Table/Cards, Vue c-admin-table)
HiddenAttribute.Areas.Edit Hide from generated editor only (Knockout CreateEdit, Vue c-admin-editor)
",3);function c(d,D,u,h,y,m){const s=t("Prop");return n(),o("div",null,[i,l(s,{def:"public Areas Area { get; set; } = Areas.All;",ctor:"1"}),p])}const C=a(r,[["render",c]]);export{b as __pageData,C as default};
diff --git a/assets/modeling_model-components_attributes_hidden.md.MV6N1IoA.lean.js b/assets/modeling_model-components_attributes_hidden.md.MV6N1IoA.lean.js
new file mode 100644
index 000000000..b0e46ecef
--- /dev/null
+++ b/assets/modeling_model-components_attributes_hidden.md.MV6N1IoA.lean.js
@@ -0,0 +1 @@
+import{_ as a,D as t,o as n,c as o,I as l,R as e}from"./chunks/framework.g9eZ-ZSs.js";const b=JSON.parse('{"title":"[Hidden]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/hidden.md","filePath":"modeling/model-components/attributes/hidden.md"}'),r={name:"modeling/model-components/attributes/hidden.md"},i=e("",6),p=e("",3);function c(d,D,u,h,y,m){const s=t("Prop");return n(),o("div",null,[i,l(s,{def:"public Areas Area { get; set; } = Areas.All;",ctor:"1"}),p])}const C=a(r,[["render",c]]);export{b as __pageData,C as default};
diff --git a/assets/modeling_model-components_attributes_inject.md.wUXXw8WO.js b/assets/modeling_model-components_attributes_inject.md.wUXXw8WO.js
new file mode 100644
index 000000000..0f39ade5b
--- /dev/null
+++ b/assets/modeling_model-components_attributes_inject.md.wUXXw8WO.js
@@ -0,0 +1,12 @@
+import{_ as s,o as a,c as n,R as e}from"./chunks/framework.g9eZ-ZSs.js";const m=JSON.parse('{"title":"[Inject]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/inject.md","filePath":"modeling/model-components/attributes/inject.md"}'),o={name:"modeling/model-components/attributes/inject.md"},l=e(`
public class Person
+{
+ public int PersonId { get; set; }
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+
+ public string GetFullName([Inject] ILogger<Person> logger)
+ {
+ logger.LogInformation("Person " + PersonId + "'s full name was requested");
+ return FirstName + " " + LastName";
+ }
+}
`,6),p=[l];function t(c,r,D,i,y,d){return a(),n("div",null,p)}const u=s(o,[["render",t]]);export{m as __pageData,u as default};
diff --git a/assets/modeling_model-components_attributes_inject.md.wUXXw8WO.lean.js b/assets/modeling_model-components_attributes_inject.md.wUXXw8WO.lean.js
new file mode 100644
index 000000000..0935d1383
--- /dev/null
+++ b/assets/modeling_model-components_attributes_inject.md.wUXXw8WO.lean.js
@@ -0,0 +1 @@
+import{_ as s,o as a,c as n,R as e}from"./chunks/framework.g9eZ-ZSs.js";const m=JSON.parse('{"title":"[Inject]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/inject.md","filePath":"modeling/model-components/attributes/inject.md"}'),o={name:"modeling/model-components/attributes/inject.md"},l=e("",6),p=[l];function t(c,r,D,i,y,d){return a(),n("div",null,p)}const u=s(o,[["render",t]]);export{m as __pageData,u as default};
diff --git a/assets/modeling_model-components_attributes_internal-use.md.J0q2AD4y.js b/assets/modeling_model-components_attributes_internal-use.md.J0q2AD4y.js
new file mode 100644
index 000000000..068a6c0b4
--- /dev/null
+++ b/assets/modeling_model-components_attributes_internal-use.md.J0q2AD4y.js
@@ -0,0 +1,20 @@
+import{_ as s,o as a,c as n,R as e}from"./chunks/framework.g9eZ-ZSs.js";const m=JSON.parse('{"title":"[InternalUse]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/internal-use.md","filePath":"modeling/model-components/attributes/internal-use.md"}'),l={name:"modeling/model-components/attributes/internal-use.md"},o=e(`
In this example, Color is the property exposed to the API, but ColorHex is the property that maps to the database that stores the value. A helper method also exists for the color generation, but needs no attribute to be hidden since methods must be explicitly exposed with [Coalesce].
If no color is saved in the database (the user hasn't picked a color), one is deterministically created.
c#
public class ApplicationUser
+{
+ public int ApplicationUserId { get; set; }
+
+ [InternalUse]
+ public string ColorHex { get; set; }
+
+ [NotMapped]
+ public string Color
+ {
+ get => ColorHex ?? GenerateColor(ApplicationUserId).ToRGBHexString();
+ set => ColorHex = value;
+ }
+
+ public static HSLColor GenerateColor(int? seed = null)
+ {
+ var random = seed.HasValue ? new Random(seed.Value) : new Random();
+ return new HSLColor(random.NextDouble(), random.Next(40, 100) / 100d, random.Next(25, 65) / 100d);
+ }
+}
`,9),p=[o];function t(r,c,D,i,y,C){return a(),n("div",null,p)}const u=s(l,[["render",t]]);export{m as __pageData,u as default};
diff --git a/assets/modeling_model-components_attributes_internal-use.md.J0q2AD4y.lean.js b/assets/modeling_model-components_attributes_internal-use.md.J0q2AD4y.lean.js
new file mode 100644
index 000000000..e19b08750
--- /dev/null
+++ b/assets/modeling_model-components_attributes_internal-use.md.J0q2AD4y.lean.js
@@ -0,0 +1 @@
+import{_ as s,o as a,c as n,R as e}from"./chunks/framework.g9eZ-ZSs.js";const m=JSON.parse('{"title":"[InternalUse]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/internal-use.md","filePath":"modeling/model-components/attributes/internal-use.md"}'),l={name:"modeling/model-components/attributes/internal-use.md"},o=e("",9),p=[o];function t(r,c,D,i,y,C){return a(),n("div",null,p)}const u=s(l,[["render",t]]);export{m as __pageData,u as default};
diff --git a/assets/modeling_model-components_attributes_list-text.md.6SOGN1hl.js b/assets/modeling_model-components_attributes_list-text.md.6SOGN1hl.js
new file mode 100644
index 000000000..fae634537
--- /dev/null
+++ b/assets/modeling_model-components_attributes_list-text.md.6SOGN1hl.js
@@ -0,0 +1,12 @@
+import{_ as s,o as a,c as n,R as e}from"./chunks/framework.g9eZ-ZSs.js";const m=JSON.parse('{"title":"[ListText]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/list-text.md","filePath":"modeling/model-components/attributes/list-text.md"}'),l={name:"modeling/model-components/attributes/list-text.md"},t=e(`
When a textual representation of an object needs to be displayed in the UI, this attribute controls which property will be used. Examples include dropdowns and cells in admin UI tables.
If this attribute is not used, and a property named Name exists on the model, that property will be used. Otherwise, the primary key will be used.
public class Person
+{
+ public int PersonId { get; set; }
+
+ public string FirstName { get; set; }
+
+ public string LastName { get; set; }
+
+ [ListText]
+ [NotMapped]
+ public string Name => FirstName + " " + LastName
+}
`,5),p=[t];function o(c,r,i,D,y,d){return a(),n("div",null,p)}const u=s(l,[["render",o]]);export{m as __pageData,u as default};
diff --git a/assets/modeling_model-components_attributes_list-text.md.6SOGN1hl.lean.js b/assets/modeling_model-components_attributes_list-text.md.6SOGN1hl.lean.js
new file mode 100644
index 000000000..6797c0afe
--- /dev/null
+++ b/assets/modeling_model-components_attributes_list-text.md.6SOGN1hl.lean.js
@@ -0,0 +1 @@
+import{_ as s,o as a,c as n,R as e}from"./chunks/framework.g9eZ-ZSs.js";const m=JSON.parse('{"title":"[ListText]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/list-text.md","filePath":"modeling/model-components/attributes/list-text.md"}'),l={name:"modeling/model-components/attributes/list-text.md"},t=e("",5),p=[t];function o(c,r,i,D,y,d){return a(),n("div",null,p)}const u=s(l,[["render",o]]);export{m as __pageData,u as default};
diff --git a/assets/modeling_model-components_attributes_load-from-data-source.md.mGs5QFTJ.js b/assets/modeling_model-components_attributes_load-from-data-source.md.mGs5QFTJ.js
new file mode 100644
index 000000000..4f9b0d56b
--- /dev/null
+++ b/assets/modeling_model-components_attributes_load-from-data-source.md.mGs5QFTJ.js
@@ -0,0 +1,11 @@
+import{_ as o,D as n,o as l,c as t,I as p,R as r,k as s,a}from"./chunks/framework.g9eZ-ZSs.js";const g=JSON.parse('{"title":"[LoadFromDataSource]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/load-from-data-source.md","filePath":"modeling/model-components/attributes/load-from-data-source.md"}'),c={name:"modeling/model-components/attributes/load-from-data-source.md"},D=r(`
Specifies that the targeted model instance method should load the instance it is called on from the specified data source when invoked from an API endpoint. If not defined, the model's default data source is used.
`,5),i=s("p",null,[a("The name of the "),s("a",{href:"/Coalesce/modeling/model-components/data-sources.html"},"Data Source"),a(" to load the instance object from.")],-1);function d(m,y,u,C,h,_){const e=n("Prop");return l(),t("div",null,[D,p(e,{def:"public Type DataSourceType { get; }",ctor:"1"}),i])}const b=o(c,[["render",d]]);export{g as __pageData,b as default};
diff --git a/assets/modeling_model-components_attributes_load-from-data-source.md.mGs5QFTJ.lean.js b/assets/modeling_model-components_attributes_load-from-data-source.md.mGs5QFTJ.lean.js
new file mode 100644
index 000000000..ed884fa44
--- /dev/null
+++ b/assets/modeling_model-components_attributes_load-from-data-source.md.mGs5QFTJ.lean.js
@@ -0,0 +1 @@
+import{_ as o,D as n,o as l,c as t,I as p,R as r,k as s,a}from"./chunks/framework.g9eZ-ZSs.js";const g=JSON.parse('{"title":"[LoadFromDataSource]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/load-from-data-source.md","filePath":"modeling/model-components/attributes/load-from-data-source.md"}'),c={name:"modeling/model-components/attributes/load-from-data-source.md"},D=r("",5),i=s("p",null,[a("The name of the "),s("a",{href:"/Coalesce/modeling/model-components/data-sources.html"},"Data Source"),a(" to load the instance object from.")],-1);function d(m,y,u,C,h,_){const e=n("Prop");return l(),t("div",null,[D,p(e,{def:"public Type DataSourceType { get; }",ctor:"1"}),i])}const b=o(c,[["render",d]]);export{g as __pageData,b as default};
diff --git a/assets/modeling_model-components_attributes_many-to-many.md.66__Haw9.js b/assets/modeling_model-components_attributes_many-to-many.md.66__Haw9.js
new file mode 100644
index 000000000..d554c82e9
--- /dev/null
+++ b/assets/modeling_model-components_attributes_many-to-many.md.66__Haw9.js
@@ -0,0 +1,9 @@
+import{_ as n,D as o,o as t,c as l,I as a,R as p,k as e}from"./chunks/framework.g9eZ-ZSs.js";const g=JSON.parse('{"title":"[ManyToMany]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/many-to-many.md","filePath":"modeling/model-components/attributes/many-to-many.md"}'),r={name:"modeling/model-components/attributes/many-to-many.md"},c=p(`
Used to specify a Many to Many relationship. Because EF core does not support automatic intermediate mapping tables, this field is used to allow for direct reference of the many-to-many collections from the ViewModel.
The named specified in the attribute will be used as the name of a collection of the objects on the other side of the relationship in the generated TypeScript ViewModels.
public class Person
+{
+ public int PersonId { get; set; }
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+
+ [ManyToMany("Appointments")]
+ public ICollection<PersonAppointment> PersonAppointments { get; set; }
+}
`,6),i=e("p",null,"The name of the collection that will contain the set of objects on the other side of the many-to-many relationship.",-1),y=e("p",null,"The name of the navigation property on the middle entity that points at the far side of the many-to-many relationship. Use this to resolve ambiguities when the middle table of the many-to-many relationship has more than two reference navigation properties on it.",-1);function D(m,d,h,u,C,f){const s=o("Prop");return t(),l("div",null,[c,a(s,{def:"public string CollectionName { get; }",ctor:"1"}),i,a(s,{def:"public string FarNavigationProperty { get; set; }"}),y])}const b=n(r,[["render",D]]);export{g as __pageData,b as default};
diff --git a/assets/modeling_model-components_attributes_many-to-many.md.66__Haw9.lean.js b/assets/modeling_model-components_attributes_many-to-many.md.66__Haw9.lean.js
new file mode 100644
index 000000000..53ec7a421
--- /dev/null
+++ b/assets/modeling_model-components_attributes_many-to-many.md.66__Haw9.lean.js
@@ -0,0 +1 @@
+import{_ as n,D as o,o as t,c as l,I as a,R as p,k as e}from"./chunks/framework.g9eZ-ZSs.js";const g=JSON.parse('{"title":"[ManyToMany]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/many-to-many.md","filePath":"modeling/model-components/attributes/many-to-many.md"}'),r={name:"modeling/model-components/attributes/many-to-many.md"},c=p("",6),i=e("p",null,"The name of the collection that will contain the set of objects on the other side of the many-to-many relationship.",-1),y=e("p",null,"The name of the navigation property on the middle entity that points at the far side of the many-to-many relationship. Use this to resolve ambiguities when the middle table of the many-to-many relationship has more than two reference navigation properties on it.",-1);function D(m,d,h,u,C,f){const s=o("Prop");return t(),l("div",null,[c,a(s,{def:"public string CollectionName { get; }",ctor:"1"}),i,a(s,{def:"public string FarNavigationProperty { get; set; }"}),y])}const b=n(r,[["render",D]]);export{g as __pageData,b as default};
diff --git a/assets/modeling_model-components_attributes_restrict.md.LYUDlSbo.js b/assets/modeling_model-components_attributes_restrict.md.LYUDlSbo.js
new file mode 100644
index 000000000..0f52bb8f0
--- /dev/null
+++ b/assets/modeling_model-components_attributes_restrict.md.LYUDlSbo.js
@@ -0,0 +1,23 @@
+import{_ as s,o as n,c as a,R as e}from"./chunks/framework.g9eZ-ZSs.js";const m=JSON.parse('{"title":"[Restrict]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/restrict.md","filePath":"modeling/model-components/attributes/restrict.md"}'),o={name:"modeling/model-components/attributes/restrict.md"},l=e(`
In addition to role-based property restrictions, you can also define property restrictions that can execute custom code for each model instance if your logic require more nuanced decisions than can be made with roles.
c#
using IntelliTect.Coalesce.DataAnnotations;
+public class Employee
+{
+ public int Id { get; set; }
+
+ [Read]
+ public string UserId { get; set; }
+
+ [Restrict<SalaryRestriction>]
+ public decimal Salary { get; set; }
+}
+
+public class SalaryRestriction(MyUserService userService) : IPropertyRestriction<Employee>
+{
+ public bool UserCanRead(IMappingContext context, string propertyName, Employee model)
+ => context.User.GetUserId() == model.UserId || userService.IsPayroll(context.User);
+
+ public bool UserCanWrite(IMappingContext context, string propertyName, Employee model, object incomingValue)
+ => userService.IsPayroll(context.User);
+
+ public bool UserCanFilter(IMappingContext context, string propertyName)
+ => userService.IsPayroll(context.User);
+}
Restriction classes support dependency injection, so you can inject any supplemental services needed to make a determination.
The UserCanRead method controls whether values of the restricted property will be mapped from model instances to the generated DTO. Similarly, UserCanWrite controls whether the property can be mapped back to the model instance from the generated DTO.
The UserCanFilter method has a default implementation that returns false, but can be implemented if there is an appropriate, instance-agnostic way to determine if a user can sort, search, or filter values of that property.
Multiple different restrictions can be placed on a single property; all of them must succeed for the operation to be permitted. Restrictions also stack on top of role attribute restrictions ([Read] and [Edit]).
A non-generic variant of IPropertyRestriction also exists for restrictions that might be reused across multiple model types.
`,8),p=[l];function t(r,c,D,y,i,C){return n(),a("div",null,p)}const u=s(o,[["render",t]]);export{m as __pageData,u as default};
diff --git a/assets/modeling_model-components_attributes_restrict.md.LYUDlSbo.lean.js b/assets/modeling_model-components_attributes_restrict.md.LYUDlSbo.lean.js
new file mode 100644
index 000000000..1110ae7ce
--- /dev/null
+++ b/assets/modeling_model-components_attributes_restrict.md.LYUDlSbo.lean.js
@@ -0,0 +1 @@
+import{_ as s,o as n,c as a,R as e}from"./chunks/framework.g9eZ-ZSs.js";const m=JSON.parse('{"title":"[Restrict]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/restrict.md","filePath":"modeling/model-components/attributes/restrict.md"}'),o={name:"modeling/model-components/attributes/restrict.md"},l=e("",8),p=[l];function t(r,c,D,y,i,C){return n(),a("div",null,p)}const u=s(o,[["render",t]]);export{m as __pageData,u as default};
diff --git a/assets/modeling_model-components_attributes_search.md.q7FurM4k.js b/assets/modeling_model-components_attributes_search.md.q7FurM4k.js
new file mode 100644
index 000000000..2202bd714
--- /dev/null
+++ b/assets/modeling_model-components_attributes_search.md.q7FurM4k.js
@@ -0,0 +1,18 @@
+import{_ as n,D as l,o as r,c as i,I as s,R as o,k as e,a as t}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"[Search]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/search.md","filePath":"modeling/model-components/attributes/search.md"}'),c={name:"modeling/model-components/attributes/search.md"},p=o(`
Coalesce supports searching through the generated API in its various implementations, including the generated list views (Table & Cards), in Select2 dropdowns, and directly through the TypeScript ListViewModels' search property.
The search parameter of the API can also be formatted as PropertyName:SearchTerm in order to search on an arbitrary property of a model. For example, a value of Nickname:Steve-o for a search term would search the Nickname property, even through it is not marked as searchable using this attribute.
By default, the system will search any field with the name 'Name'. If this doesn't exist, the ID is used as the only searchable field. Once you place the Search attribute on one or more properties on a model, only those annotated properties will be searched.
If the input is a parsable date, rows will be searched based on that date.
Date search will do its best to guess at the user's intentions:
Various forms of year/month combos are supported, and if only a year/month is inputted, it will look for all dates in that month, e.g. "Feb 2017" or "2016-11".
A date without a time (or a time of exactly midnight) will search the entire day, e.g. "2017/4/18".
A date/time with minutes and seconds equal to 0 will search the entire hour, e.g. "April 7, 2017 11 AM".
TIP
When searching on date properties, you should almost always set IsSplitOnSpaces = false on the Search attribute. This allows natural inputs like "July 21, 2017" to search correctly. Otherwise, only non-whitespace date formats will work, like "2017/21/07".
When a reference navigation property is marked with [Search], searchable properties on the referenced object will also be searched. This behavior will go up to two levels away from the root object, and can be controlled with the RootWhitelist and RootBlacklist properties on the [Search] attribute that are outlined below.
When a collection navigation property is marked with [Search], searchable properties on the child objects will also be searched. This behavior will go up to two levels away from the root object, and can be controlled with the RootWhitelist and RootBlacklist properties on the [Search] attribute that are outlined below.
WARNING
Searches on collection navigation properties usually don't translate well with EF Core, leading to potentially degraded performance. Use this feature cautiously.
`,24),h=e("p",null,[t("If set to true (the default), each word in the search terms will be searched for in each searchable field independently, and a row will only be considered a match if each word in the search term is a match on at least one searchable property where "),e("code",null,"IsSplitOnSpaces == true")],-1),d=e("p",null,[t("This is useful when searching for a full name across two or more fields. In the above example, using "),e("code",null,"IsSplitOnSpaces = true"),t(" would provide more intuitive behavior since it will search both first name and last name for each word entered into the search field. But, "),e("a",{href:"https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/",target:"_blank",rel:"noreferrer"},"you probably shouldn't be doing that"),t(".")],-1),u=o("
For string properties, specifies how the value in the property/column will be matched.
BeginsWith: Search term will be checked for at the beginning of the field's value in a case insensitive manner.
Equals: Search term must match the field exactly in a case insensitive manner.
EqualsNatural: Search term must match exactly, using the natural casing handling of the evaluation environment. Default database collation will be used if evaluated in SQL, and exact casing will be used if evaluated in memory. This allows index seeks to be used instead of index scans, providing extra high performance searches against indexed columns
Contains: Search term will be checked for anywhere inside the field's value in a case insensitive manner. Will be slow against large databases - performance cannot be improved with database indexing.
",2),D=e("p",null,"A comma-delimited list of model class names that, if set, will prevent the targeted property from being searched unless the root object of the API call was one of the specified class names.",-1),m=e("p",null,"A comma-delimited list of model class names that, if set, will prevent the targeted property from being searched if the root object of the API call was one of the specified class names.",-1);function y(b,f,g,w,C,v){const a=l("Prop");return r(),i("div",null,[p,s(a,{def:"public bool IsSplitOnSpaces { get; set; } = true;"}),h,d,s(a,{def:"public SearchMethods SearchMethod { get; set; } = SearchMethods.BeginsWith;"}),u,s(a,{def:"public string RootWhitelist { get; set; } = null;"}),D,s(a,{def:"public string RootBlacklist { get; set; } = null;"}),m])}const k=n(c,[["render",y]]);export{_ as __pageData,k as default};
diff --git a/assets/modeling_model-components_attributes_search.md.q7FurM4k.lean.js b/assets/modeling_model-components_attributes_search.md.q7FurM4k.lean.js
new file mode 100644
index 000000000..09206395d
--- /dev/null
+++ b/assets/modeling_model-components_attributes_search.md.q7FurM4k.lean.js
@@ -0,0 +1 @@
+import{_ as n,D as l,o as r,c as i,I as s,R as o,k as e,a as t}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"[Search]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/search.md","filePath":"modeling/model-components/attributes/search.md"}'),c={name:"modeling/model-components/attributes/search.md"},p=o("",24),h=e("p",null,[t("If set to true (the default), each word in the search terms will be searched for in each searchable field independently, and a row will only be considered a match if each word in the search term is a match on at least one searchable property where "),e("code",null,"IsSplitOnSpaces == true")],-1),d=e("p",null,[t("This is useful when searching for a full name across two or more fields. In the above example, using "),e("code",null,"IsSplitOnSpaces = true"),t(" would provide more intuitive behavior since it will search both first name and last name for each word entered into the search field. But, "),e("a",{href:"https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/",target:"_blank",rel:"noreferrer"},"you probably shouldn't be doing that"),t(".")],-1),u=o("",2),D=e("p",null,"A comma-delimited list of model class names that, if set, will prevent the targeted property from being searched unless the root object of the API call was one of the specified class names.",-1),m=e("p",null,"A comma-delimited list of model class names that, if set, will prevent the targeted property from being searched if the root object of the API call was one of the specified class names.",-1);function y(b,f,g,w,C,v){const a=l("Prop");return r(),i("div",null,[p,s(a,{def:"public bool IsSplitOnSpaces { get; set; } = true;"}),h,d,s(a,{def:"public SearchMethods SearchMethod { get; set; } = SearchMethods.BeginsWith;"}),u,s(a,{def:"public string RootWhitelist { get; set; } = null;"}),D,s(a,{def:"public string RootBlacklist { get; set; } = null;"}),m])}const k=n(c,[["render",y]]);export{_ as __pageData,k as default};
diff --git a/assets/modeling_model-components_attributes_security-attribute.md.jOlvg0wG.js b/assets/modeling_model-components_attributes_security-attribute.md.jOlvg0wG.js
new file mode 100644
index 000000000..2bf34c930
--- /dev/null
+++ b/assets/modeling_model-components_attributes_security-attribute.md.jOlvg0wG.js
@@ -0,0 +1,27 @@
+import{_ as l,D as n,o as r,c as p,I as o,a as e,R as t,k as s}from"./chunks/framework.g9eZ-ZSs.js";const v=JSON.parse('{"title":"Security Attributes","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/security-attribute.md","filePath":"modeling/model-components/attributes/security-attribute.md"}'),c={name:"modeling/model-components/attributes/security-attribute.md"},i=t(`
Coalesce provides a collection of attributes which can provide class-level (and property-level, where appropriate) security controls over the generated API.
TIP
This page provides API-level documentation for a specific set of attributes. For a complete overview of all the security-focused techniques that can be used in a Coalesce application, see the Security page.
There are important differences between class-level security and property-level security, beyond the usage of the attributes themselves. In general, class-level security is implemented in the generated API Controllers as [Authorize] attributes on the generated actions. Property security attributes are implemented in the Generated C# DTOs.
Controls permissions for editing of objects and properties through the API.
For property-level security only, if a [Read] attribute is present, one of its roles must be fulfilled in addition to the roles specified (if any) for the [Edit] attribute.
For property security, [Read] and [Edit] can be used to apply role-based security. If you need logic more complicated than checking for the presence of a role, [Restrict] offers the ability to write custom code to control the read and/or write permissions of a property.
`,30),D=s("p",null,"A comma-delimited list of roles that are authorized to take perform the action represented by the attribute. If the current user belongs to any of the listed roles, the action will be allowed.",-1),d=s("p",null,[e("The string set for this property will be outputted as an "),s("code",null,'[Authorize(Roles="RolesString")]'),e(" attribute on generated API controller actions.")],-1),u=t("
The level of access to allow for the action for class-level security only. Has no effect for property-level security.
Enum values are:
SecurityPermissionLevels.AllowAll Allow all users to perform the action for the attribute, including users who are not authenticated at all.
SecurityPermissionLevels.AllowAuthorized Allow only users who are members of the roles specified on the attribute to perform the action. If no roles are specified on the attribute, then all authenticated users are allowed (no anonymous access).
SecurityPermissionLevels.DenyAll Deny the action to all users, regardless of authentication status or authorization level. If DenyAll is used, no API endpoint for the action will be generated.
",3);function y(h,m,C,b,f,g){const a=n("Prop");return r(),p("div",null,[i,o(a,{def:"public string Roles { get; set; }",ctor:"1"}),e(),D,d,o(a,{def:"public SecurityPermissionLevels PermissionLevel { get; set; }",ctor:"2"}),e(),u])}const P=l(c,[["render",y]]);export{v as __pageData,P as default};
diff --git a/assets/modeling_model-components_attributes_security-attribute.md.jOlvg0wG.lean.js b/assets/modeling_model-components_attributes_security-attribute.md.jOlvg0wG.lean.js
new file mode 100644
index 000000000..831a4e22d
--- /dev/null
+++ b/assets/modeling_model-components_attributes_security-attribute.md.jOlvg0wG.lean.js
@@ -0,0 +1 @@
+import{_ as l,D as n,o as r,c as p,I as o,a as e,R as t,k as s}from"./chunks/framework.g9eZ-ZSs.js";const v=JSON.parse('{"title":"Security Attributes","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/security-attribute.md","filePath":"modeling/model-components/attributes/security-attribute.md"}'),c={name:"modeling/model-components/attributes/security-attribute.md"},i=t("",30),D=s("p",null,"A comma-delimited list of roles that are authorized to take perform the action represented by the attribute. If the current user belongs to any of the listed roles, the action will be allowed.",-1),d=s("p",null,[e("The string set for this property will be outputted as an "),s("code",null,'[Authorize(Roles="RolesString")]'),e(" attribute on generated API controller actions.")],-1),u=t("",3);function y(h,m,C,b,f,g){const a=n("Prop");return r(),p("div",null,[i,o(a,{def:"public string Roles { get; set; }",ctor:"1"}),e(),D,d,o(a,{def:"public SecurityPermissionLevels PermissionLevel { get; set; }",ctor:"2"}),e(),u])}const P=l(c,[["render",y]]);export{v as __pageData,P as default};
diff --git a/assets/modeling_model-components_attributes_select-filter.md.O79G0Zlu.js b/assets/modeling_model-components_attributes_select-filter.md.O79G0Zlu.js
new file mode 100644
index 000000000..73ec24622
--- /dev/null
+++ b/assets/modeling_model-components_attributes_select-filter.md.O79G0Zlu.js
@@ -0,0 +1,19 @@
+import{_ as o,D as n,o as p,c as t,I as l,R as c,k as s,a as e}from"./chunks/framework.g9eZ-ZSs.js";const v=JSON.parse('{"title":"[SelectFilter]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/select-filter.md","filePath":"modeling/model-components/attributes/select-filter.md"}'),r={name:"modeling/model-components/attributes/select-filter.md"},D=c(`
This attribute only affects the generated Knockout HTML views - it does not enforce any relational rules in your data.
This attribute also currently has no effect against the Vue stack.
Specify a property to restrict dropdown menus by. Values presented will be only those where the value of the foreign property matches the value of the local property.
The local property name defaults to the same value of the foreign property.
Additionally, in place of a LocalPropertyName to check against, you may instead specify a static value using StaticPropertyValue to filter by a constant.
In this example, a dropdown for EmployeeRank created using @Knockout.SelectForObject in cshtml files will only present possible values of EmployeeRank which are valid for the EmployeeType of the Employee.
c#
public class Employee
+{
+ public int EmployeeId { get; set; }
+ public int EmployeeTypeId { get; set; }
+ public EmployeeType EmployeeType { get; set; }
+ public int EmployeeRankId { get; set; }
+
+ [SelectFilter(ForeignPropertyName = nameof(EmployeeRank.EmployeeTypeId), LocalPropertyName = nameof(Employee.EmployeeTypeId))]
+ public EmployeeRank EmployeeRank { get; set; }
+}
+
+public class EmployeeRank
+{
+ public int EmployeeRankId { get; set; }
+ public int EmployeeTypeId { get; set; }
+ public EmployeeType EmployeeType { get; set; }
+}
`,10),y=s("p",null,"The name of the property on the foreign object to filter against.",-1),i=s("p",null,"The name of another property belonging to the class in which this attribute is used. The results of select lists will be filtered to match this value.",-1),d=s("p",null,[e("Defaults to the value of "),s("code",null,"ForeignPropertyName"),e(" if not set.")],-1),C=s("p",null,[e("If specified, the "),s("code",null,"LocalPropertyName"),e(" will be resolved from the property by this name that resides on the local object.")],-1),u=s("p",null,"This allows for querying against properties that are one level away from the current object.",-1),h=s("p",null,[e("A constant value that the foreign property will be filtered against. This string must be parsable into the foreign property's type to have any effect. If this is set, "),s("code",null,"LocalPropertyName"),e(" will be ignored.")],-1);function m(f,g,E,b,_,F){const a=n("Prop");return p(),t("div",null,[D,l(a,{def:"public string ForeignPropertyName { get; set; }"}),y,l(a,{def:"public string LocalPropertyName { get; set; }"}),i,d,l(a,{def:"public string LocalPropertyObjectName { get; set; }"}),C,u,l(a,{def:"public string StaticPropertyValue { get; set; }"}),h])}const T=o(r,[["render",m]]);export{v as __pageData,T as default};
diff --git a/assets/modeling_model-components_attributes_select-filter.md.O79G0Zlu.lean.js b/assets/modeling_model-components_attributes_select-filter.md.O79G0Zlu.lean.js
new file mode 100644
index 000000000..ccd4ef210
--- /dev/null
+++ b/assets/modeling_model-components_attributes_select-filter.md.O79G0Zlu.lean.js
@@ -0,0 +1 @@
+import{_ as o,D as n,o as p,c as t,I as l,R as c,k as s,a as e}from"./chunks/framework.g9eZ-ZSs.js";const v=JSON.parse('{"title":"[SelectFilter]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/select-filter.md","filePath":"modeling/model-components/attributes/select-filter.md"}'),r={name:"modeling/model-components/attributes/select-filter.md"},D=c("",10),y=s("p",null,"The name of the property on the foreign object to filter against.",-1),i=s("p",null,"The name of another property belonging to the class in which this attribute is used. The results of select lists will be filtered to match this value.",-1),d=s("p",null,[e("Defaults to the value of "),s("code",null,"ForeignPropertyName"),e(" if not set.")],-1),C=s("p",null,[e("If specified, the "),s("code",null,"LocalPropertyName"),e(" will be resolved from the property by this name that resides on the local object.")],-1),u=s("p",null,"This allows for querying against properties that are one level away from the current object.",-1),h=s("p",null,[e("A constant value that the foreign property will be filtered against. This string must be parsable into the foreign property's type to have any effect. If this is set, "),s("code",null,"LocalPropertyName"),e(" will be ignored.")],-1);function m(f,g,E,b,_,F){const a=n("Prop");return p(),t("div",null,[D,l(a,{def:"public string ForeignPropertyName { get; set; }"}),y,l(a,{def:"public string LocalPropertyName { get; set; }"}),i,d,l(a,{def:"public string LocalPropertyObjectName { get; set; }"}),C,u,l(a,{def:"public string StaticPropertyValue { get; set; }"}),h])}const T=o(r,[["render",m]]);export{v as __pageData,T as default};
diff --git a/assets/modeling_model-components_attributes_typescript-partial.md.4Crd20VW.js b/assets/modeling_model-components_attributes_typescript-partial.md.4Crd20VW.js
new file mode 100644
index 000000000..e5ac9a3ae
--- /dev/null
+++ b/assets/modeling_model-components_attributes_typescript-partial.md.4Crd20VW.js
@@ -0,0 +1,7 @@
+import{_ as a,D as s,o as t,c as o,I as n,R as l,k as p}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"[TypeScriptPartial]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/typescript-partial.md","filePath":"modeling/model-components/attributes/typescript-partial.md"}'),r={name:"modeling/model-components/attributes/typescript-partial.md"},i=l(`
This attribute only applies to the Knockout front-end stack. It is not applicable to the Vue stack.
If defined on a model, a typescript file will be generated in ./Scripts/Partials if one does not already exist. This 'Partial' TypeScript file contains a class which inherits from the generated TypeScript ViewModel. The partial class has the same name as the generated ViewModel would normally have, and the generated ViewModel is renamed to "<ClassName>Partial".
This behavior allows you to extend the behavior of the generated TypeScript view models with your own properties and methods for defining more advanced behavior on the client. One of the most common use cases of this is to define additional Knockout ComputedObservable properties for information that is only useful in the browser - for example, computing a css class based on data in the object.
`,7),c=p("p",null,"If set, overrides the name of the generated ViewModel which becomes the base class for the generated 'Partial' TypeScript file.",-1);function d(h,m,y,u,D,f){const e=s("Prop");return t(),o("div",null,[i,n(e,{def:"public string BaseClassName { get; set; }"}),c])}const g=a(r,[["render",d]]);export{_ as __pageData,g as default};
diff --git a/assets/modeling_model-components_attributes_typescript-partial.md.4Crd20VW.lean.js b/assets/modeling_model-components_attributes_typescript-partial.md.4Crd20VW.lean.js
new file mode 100644
index 000000000..767e004e4
--- /dev/null
+++ b/assets/modeling_model-components_attributes_typescript-partial.md.4Crd20VW.lean.js
@@ -0,0 +1 @@
+import{_ as a,D as s,o as t,c as o,I as n,R as l,k as p}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"[TypeScriptPartial]","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/attributes/typescript-partial.md","filePath":"modeling/model-components/attributes/typescript-partial.md"}'),r={name:"modeling/model-components/attributes/typescript-partial.md"},i=l("",7),c=p("p",null,"If set, overrides the name of the generated ViewModel which becomes the base class for the generated 'Partial' TypeScript file.",-1);function d(h,m,y,u,D,f){const e=s("Prop");return t(),o("div",null,[i,n(e,{def:"public string BaseClassName { get; set; }"}),c])}const g=a(r,[["render",d]]);export{_ as __pageData,g as default};
diff --git a/assets/modeling_model-components_behaviors.md.34ydZMzc.js b/assets/modeling_model-components_behaviors.md.34ydZMzc.js
new file mode 100644
index 000000000..1b23f219b
--- /dev/null
+++ b/assets/modeling_model-components_behaviors.md.34ydZMzc.js
@@ -0,0 +1,70 @@
+import{_ as o,D as l,o as r,c as p,I as t,R as n,k as e,a as s}from"./chunks/framework.g9eZ-ZSs.js";const K=JSON.parse('{"title":"Behaviors","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/behaviors.md","filePath":"modeling/model-components/behaviors.md"}'),c={name:"modeling/model-components/behaviors.md"},i=n(`
In a CRUD system, creating, updating, and deleting are considered especially different from reading. In Coalesce, the dedicated classes that perform these operations are derivatives of a special interface known as theIBehaviors<T>. These are their stories.
Coalesce separates out the parts of your API that read your data from the parts that mutate it. The read portion is performed by Data Sources, and the mutations are performed by behaviors. Like data sources, there exists a standard set of behaviors that Coalesce provides out-of-the-box that cover the most common use cases for creating, updating, and deleting objects in your data model.
Also like data sources, these functions can be easily overridden on a per-model basis, allowing complete control over the ways in which your data is mutated by the APIs that Coalesce generates. However, unlike data sources which can have as many implementations per model as you like, you can only have one set of behaviors.
By default, each of your models that Coalesce exposes will utilize the standard behaviors (IntelliTect.Coalesce.StandardBehaviors<T, TContext>) for the out-of-the-box API endpoints that Coalesce provides. These behaviors provide a set of create, update, and delete methods for an EF Core DbContext, as well as a plethora of virtual methods that make the StandardBehaviors a great base class for your custom implementations. Unlike data sources which require an annotation to override the Coalesce-provided standard class, the simple presence of an explicitly declared set of behaviors will suppress the standard behaviors.
Note
When you define a set of custom behaviors, take note that these are only used by the standard set of API endpoints that Coalesce always provides. They will not be used to handle any mutations in any Methods you write for your models.
To create your own behaviors, you simply need to define a class that implements IntelliTect.Coalesce.IBehaviors<T>. To expose your behaviors to Coalesce, either place it as a nested class of the type T that your behaviors are for, or annotate it with the [Coalesce] attribute. Of course, the easiest way to create behaviors that doesn't require you to re-engineer a great deal of logic would be to inherit from IntelliTect.Coalesce.StandardBehaviors<T, TContext>, and then override only the parts that you need.
c#
public class Case
+{
+ public int CaseId { get; set; }
+ public int OwnerId { get; set; }
+ public bool IsDeleted { get; set; }
+ ...
+}
+
+[Coalesce]
+public class CaseBehaviors : StandardBehaviors<Case, AppDbContext>
+{
+ public CaseBehaviors(CrudContext<AppDbContext> context) : base(context) { }
+
+ public override ItemResult BeforeSave(SaveKind kind, Case oldItem, Case item)
+ {
+ // Allow admins to bypass all validation.
+ if (User.IsInRole("Admin")) return true;
+
+ if (kind == SaveKind.Update && oldItem.OwnerId != item.OwnerId)
+ return "The owner of a case may not be changed";
+
+ // This is a new item, OR its an existing item and the owner isn't being modified.
+ if (item.CreatedById != User.GetUserId())
+ return "You are not the owner of this item.";
+
+ return true;
+ }
+
+ public override ItemResult BeforeDelete(Case item)
+ => User.IsInRole("Manager") ? true : "Unauthorized";
+
+ public override Task ExecuteDeleteAsync(Case item)
+ {
+ // Soft delete the item.
+ item.IsDeleted = true;
+ return Db.SaveChangesAsync();
+ }
+}
All behaviors are instantiated using dependency injection and your application's IServiceProvider. As a result, you can add whatever constructor parameters you desire to your behaviors as long as a value for them can be resolved from your application's services. The single parameter to the StandardBehaviors is resolved in this way - the CrudContext<TContext> contains the common set of objects most commonly used, including the DbContext and the ClaimsPrincipal representing the current user.
The standard behaviors, IntelliTect.Coalesce.StandardBehaviors<T> and its EntityFramework-supporting sibling IntelliTect.Coalesce.StandardBehaviors<T, TContext>, contain a significant number of properties and methods that can be utilized and/or overridden at your leisure.
`,15),d=e("p",null,"The object passed to the constructor that contains the set of objects needed by the standard behaviors, and those that are most likely to be used in custom implementations.",-1),D=e("p",null,[s("An instance of the db context that contains a "),e("code",null,"DbSet"),s(" for the entity handled by the behaviors")],-1),h=e("p",null,"The user making the current request.",-1),y=e("p",null,"A data source that, if set, will override the data source that is used to retrieve the target of an update operation from the database. The incoming values will then be set on this retrieved object. Null by default; override by setting a value in the constructor.",-1),u=e("p",null,"A data source that, if set, will override the data source that is used to retrieve a newly-created or just-updated object from the database after a save. The retrieved object will be returned to the client. Null by default; override by setting a value in the constructor.",-1),m=e("p",null,"A data source that, if set, will override the data source that is used to retrieve the target of an delete operation from the database. The retrieved object will then be deleted. Null by default; override by setting a value in the constructor.",-1),C=n(`
A data source that, if set, will override the data source that is used to retrieve the target of an delete operation from the database after it has been deleted. If an object is able to be retrieved from this data source, it will be sent back to the client. This allows soft-deleted items to be returned to the client when the user is able to see them. Null by default; override by setting a value in the constructor.
The standard behaviors implementation contains many different methods which can be overridden in your derived class to control functionality.
These methods often call one another, so overriding one method may cause some other method to no longer be called. The hierarchy of method calls, ignoring any logic or conditions contained within, is as follows:
All of the methods outlined above can be overridden. A description of each of the methods is as follows:
`,7),v=e("p",null,[s("Save the given item. This is the main entry point for saving, and takes a DTO as a parameter. This method is responsible for performing mapping to your EF models and ultimately saving to your database. If it is required that you access properties from the incoming DTO in this method, a set of extension methods "),e("code",null,"GetValue"),s(" and "),e("code",null,"GetObject"),s(" are available on the DTO for accessing properties that are mapped 1:1 with your EF models.")],-1),b=e("p",null,"Given the incoming DTO on which Save has been called, examine its properties to determine if the operation is meant to be a create or an update operation. Return this distinction along with the key that was used to make the distinction.",-1),f=e("p",null,"This method is called outside of the standard data source by the base API controller to perform role-based security on saves at the controller level.",-1),T=e("p",null,[s("Returns a "),e("code",null,"DbSet"),s(" that items can be added to (creates) or remove from (deletes).")],-1),g=e("p",null,[s("Provides a chance to validate the properties of the DTO object itself, as opposed to doing validation in "),e("code",null,"BeforeSave"),s(" of the properties of the model after the DTO has been mapped to the model. This also where "),e("a",{href:"/Coalesce/topics/security.html#attribute-validation"},"attribute-based validation"),s(" is performed.")],-1),_=e("p",null,[s("To perform custom validation in this method (uncommon), there are a number of extension methods on "),e("code",null,"IClassDto"),s(" that can be used to access the value of the properties of "),e("a",{href:"/Coalesce/stacks/agnostic/dtos.html"},"Generated C# DTOs"),s(". For behaviors on "),e("a",{href:"/Coalesce/modeling/model-types/dtos.html"},"Custom DTOs"),s(" where the DTO type is known, simply cast to the correct type.")],-1),w=n("
Map the properties of the incoming DTO to the model that will be saved to the database. For a SaveKind.Create, this will call the MapToNew method on the DTO and a new instance must be returned (item will be null). For a SaveKind.Update, this will call the MapTo method on the DTO, and the incoming item must be returned. If more precise control is needed, extension methods on IClassDto<T> or casting to a known type can be used to get specific values. If all else fails, the DTO can be reflected upon.
",1),S=e("p",null,"Provides an easy way for derived classes to intercept a save attempt and either reject it by returning an unsuccessful result, or approve it by returning success. The incoming item can also be modified at will in this method to override changes that the client made as desired.",-1),I=e("p",null,[s("Provides an easy way for derived classes to perform actions after a save operation has been completed. Failure results returned here will present an error to the client, but will not prevent modifications to the database since changes have already been saved at this point. This method can optionally modify or replace the item that is sent back to the client after a save by setting "),e("code",null,"ref T item"),s(" to another object or to null. Setting "),e("code",null,"ref IncludeTree includeTree"),s(" will override the "),e("a",{href:"/Coalesce/concepts/include-tree.html"},"Include Tree"),s(" used to shape the response object.")],-1),A=e("div",{class:"warning custom-block"},[e("p",{class:"custom-block-title"},"WARNING"),e("p",null,[s("Setting "),e("code",null,"ref T item"),s(" to null will prevent the new object from being returned - be aware that this can be harmful in create scenarios since it prevents the client from receiving the primary key of the newly created item. If autoSave is enabled on the client, this could cause a large number of duplicate objects to be created in the database, since each subsequent save by the client will be treated as a create when the incoming object lacks a primary key.")])],-1),E=e("p",null,"Deletes the given item.",-1),B=e("p",null,"Provides an easy way to intercept a delete request and potentially reject it (by returning a non-success ItemResult).",-1),k=e("p",null,[s("Performs the delete action against the database. The implementation of this method removes the item from its corresponding "),e("code",null,"DbSet"),s(", and then calls "),e("code",null,"Db.SaveChangesAsync()"),s(".")],-1),x=e("p",null,"Overriding this allows for changing this row-deletion implementation to something else, like setting of a soft delete flag, or copying the data into another archival table before deleting.",-1),F=n(`
Allows for performing any sort of cleanup actions after a delete has completed. If the item was still able to be retrieved from the database after the delete operation completed, this method allows lets you modify or replace the item that is sent back to the client by setting ref T item to another object or to null. Setting ref IncludeTree includeTree will override the Include Tree used to shape the response object.
You can, of course, create a custom base behaviors class that all your custom implementations inherit from. But, what if you want to override the standard behaviors across your entire application, so that StandardBehaviors<,> will never be instantiated? You can do that too!
Simply create a class that implements IEntityFrameworkBehaviors<,> (the StandardBehaviors<,> already does - feel free to inherit from it), then register it at application startup like so:
c#
public class MyBehaviors<T, TContext> : StandardBehaviors<T, TContext>
+ where T : class
+ where TContext : DbContext
+{
+ public MyBehaviors(CrudContext<TContext> context) : base(context)
+ {
+ }
+
+ ...
+}
Your custom behaviors class must have the same generic type parameters - <T, TContext>. Otherwise, the Microsoft.Extensions.DependencyInjection service provider won't know how to inject it.
`,7);function P(q,j,O,R,N,M){const a=l("Prop");return r(),p("div",null,[i,t(a,{def:"CrudContext Context"}),d,t(a,{def:"TContext Db"}),D,t(a,{def:"ClaimsPrincipal User"}),h,t(a,{def:"IDataSource OverrideFetchForUpdateDataSource"}),y,t(a,{def:"IDataSource OverridePostSaveResultDataSource"}),u,t(a,{def:"IDataSource OverrideFetchForDeleteDataSource"}),m,t(a,{def:"IDataSource OverridePostDeleteResultDataSource"}),C,t(a,{def:"Task> SaveAsync(TDto incomingDto, IDataSource dataSource, IDataSourceParameters parameters)"}),v,t(a,{def:"Task<(SaveKind Kind, object? IncomingKey)> DetermineSaveKindAsync(TDto incomingDto, IDataSource dataSource, IDataSourceParameters parameters)"}),b,f,t(a,{def:"DbSet GetDbSet()"}),T,t(a,{def:"ItemResult ValidateDto(SaveKind kind, IClassDto dto)"}),g,_,t(a,{def:"T MapIncomingDto(SaveKind kind, T? item, TDto dto, IDataSourceParameters parameters)"}),w,t(a,{def:`Task BeforeSaveAsync(SaveKind kind, T? oldItem, T item);
+ItemResult BeforeSave(SaveKind kind, T? oldItem, T item)`}),S,t(a,{def:"ItemResult AfterSave(SaveKind kind, T? oldItem, ref T item, ref IncludeTree? includeTree)"}),I,A,t(a,{def:"Task> DeleteAsync(object id, IDataSource dataSource, IDataSourceParameters parameters)"}),E,t(a,{def:`Task BeforeDeleteAsync(T item);
+ItemResult BeforeDelete(T item)`}),B,t(a,{def:"Task ExecuteDeleteAsync(T item)"}),k,x,t(a,{def:"void AfterDelete(ref T item, ref IncludeTree? includeTree)"}),F])}const U=o(c,[["render",P]]);export{K as __pageData,U as default};
diff --git a/assets/modeling_model-components_behaviors.md.34ydZMzc.lean.js b/assets/modeling_model-components_behaviors.md.34ydZMzc.lean.js
new file mode 100644
index 000000000..8be77813d
--- /dev/null
+++ b/assets/modeling_model-components_behaviors.md.34ydZMzc.lean.js
@@ -0,0 +1,3 @@
+import{_ as o,D as l,o as r,c as p,I as t,R as n,k as e,a as s}from"./chunks/framework.g9eZ-ZSs.js";const K=JSON.parse('{"title":"Behaviors","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/behaviors.md","filePath":"modeling/model-components/behaviors.md"}'),c={name:"modeling/model-components/behaviors.md"},i=n("",15),d=e("p",null,"The object passed to the constructor that contains the set of objects needed by the standard behaviors, and those that are most likely to be used in custom implementations.",-1),D=e("p",null,[s("An instance of the db context that contains a "),e("code",null,"DbSet"),s(" for the entity handled by the behaviors")],-1),h=e("p",null,"The user making the current request.",-1),y=e("p",null,"A data source that, if set, will override the data source that is used to retrieve the target of an update operation from the database. The incoming values will then be set on this retrieved object. Null by default; override by setting a value in the constructor.",-1),u=e("p",null,"A data source that, if set, will override the data source that is used to retrieve a newly-created or just-updated object from the database after a save. The retrieved object will be returned to the client. Null by default; override by setting a value in the constructor.",-1),m=e("p",null,"A data source that, if set, will override the data source that is used to retrieve the target of an delete operation from the database. The retrieved object will then be deleted. Null by default; override by setting a value in the constructor.",-1),C=n("",7),v=e("p",null,[s("Save the given item. This is the main entry point for saving, and takes a DTO as a parameter. This method is responsible for performing mapping to your EF models and ultimately saving to your database. If it is required that you access properties from the incoming DTO in this method, a set of extension methods "),e("code",null,"GetValue"),s(" and "),e("code",null,"GetObject"),s(" are available on the DTO for accessing properties that are mapped 1:1 with your EF models.")],-1),b=e("p",null,"Given the incoming DTO on which Save has been called, examine its properties to determine if the operation is meant to be a create or an update operation. Return this distinction along with the key that was used to make the distinction.",-1),f=e("p",null,"This method is called outside of the standard data source by the base API controller to perform role-based security on saves at the controller level.",-1),T=e("p",null,[s("Returns a "),e("code",null,"DbSet"),s(" that items can be added to (creates) or remove from (deletes).")],-1),g=e("p",null,[s("Provides a chance to validate the properties of the DTO object itself, as opposed to doing validation in "),e("code",null,"BeforeSave"),s(" of the properties of the model after the DTO has been mapped to the model. This also where "),e("a",{href:"/Coalesce/topics/security.html#attribute-validation"},"attribute-based validation"),s(" is performed.")],-1),_=e("p",null,[s("To perform custom validation in this method (uncommon), there are a number of extension methods on "),e("code",null,"IClassDto"),s(" that can be used to access the value of the properties of "),e("a",{href:"/Coalesce/stacks/agnostic/dtos.html"},"Generated C# DTOs"),s(". For behaviors on "),e("a",{href:"/Coalesce/modeling/model-types/dtos.html"},"Custom DTOs"),s(" where the DTO type is known, simply cast to the correct type.")],-1),w=n("",1),S=e("p",null,"Provides an easy way for derived classes to intercept a save attempt and either reject it by returning an unsuccessful result, or approve it by returning success. The incoming item can also be modified at will in this method to override changes that the client made as desired.",-1),I=e("p",null,[s("Provides an easy way for derived classes to perform actions after a save operation has been completed. Failure results returned here will present an error to the client, but will not prevent modifications to the database since changes have already been saved at this point. This method can optionally modify or replace the item that is sent back to the client after a save by setting "),e("code",null,"ref T item"),s(" to another object or to null. Setting "),e("code",null,"ref IncludeTree includeTree"),s(" will override the "),e("a",{href:"/Coalesce/concepts/include-tree.html"},"Include Tree"),s(" used to shape the response object.")],-1),A=e("div",{class:"warning custom-block"},[e("p",{class:"custom-block-title"},"WARNING"),e("p",null,[s("Setting "),e("code",null,"ref T item"),s(" to null will prevent the new object from being returned - be aware that this can be harmful in create scenarios since it prevents the client from receiving the primary key of the newly created item. If autoSave is enabled on the client, this could cause a large number of duplicate objects to be created in the database, since each subsequent save by the client will be treated as a create when the incoming object lacks a primary key.")])],-1),E=e("p",null,"Deletes the given item.",-1),B=e("p",null,"Provides an easy way to intercept a delete request and potentially reject it (by returning a non-success ItemResult).",-1),k=e("p",null,[s("Performs the delete action against the database. The implementation of this method removes the item from its corresponding "),e("code",null,"DbSet"),s(", and then calls "),e("code",null,"Db.SaveChangesAsync()"),s(".")],-1),x=e("p",null,"Overriding this allows for changing this row-deletion implementation to something else, like setting of a soft delete flag, or copying the data into another archival table before deleting.",-1),F=n("",7);function P(q,j,O,R,N,M){const a=l("Prop");return r(),p("div",null,[i,t(a,{def:"CrudContext Context"}),d,t(a,{def:"TContext Db"}),D,t(a,{def:"ClaimsPrincipal User"}),h,t(a,{def:"IDataSource OverrideFetchForUpdateDataSource"}),y,t(a,{def:"IDataSource OverridePostSaveResultDataSource"}),u,t(a,{def:"IDataSource OverrideFetchForDeleteDataSource"}),m,t(a,{def:"IDataSource OverridePostDeleteResultDataSource"}),C,t(a,{def:"Task> SaveAsync(TDto incomingDto, IDataSource dataSource, IDataSourceParameters parameters)"}),v,t(a,{def:"Task<(SaveKind Kind, object? IncomingKey)> DetermineSaveKindAsync(TDto incomingDto, IDataSource dataSource, IDataSourceParameters parameters)"}),b,f,t(a,{def:"DbSet GetDbSet()"}),T,t(a,{def:"ItemResult ValidateDto(SaveKind kind, IClassDto dto)"}),g,_,t(a,{def:"T MapIncomingDto(SaveKind kind, T? item, TDto dto, IDataSourceParameters parameters)"}),w,t(a,{def:`Task BeforeSaveAsync(SaveKind kind, T? oldItem, T item);
+ItemResult BeforeSave(SaveKind kind, T? oldItem, T item)`}),S,t(a,{def:"ItemResult AfterSave(SaveKind kind, T? oldItem, ref T item, ref IncludeTree? includeTree)"}),I,A,t(a,{def:"Task> DeleteAsync(object id, IDataSource dataSource, IDataSourceParameters parameters)"}),E,t(a,{def:`Task BeforeDeleteAsync(T item);
+ItemResult BeforeDelete(T item)`}),B,t(a,{def:"Task ExecuteDeleteAsync(T item)"}),k,x,t(a,{def:"void AfterDelete(ref T item, ref IncludeTree? includeTree)"}),F])}const U=o(c,[["render",P]]);export{K as __pageData,U as default};
diff --git a/assets/modeling_model-components_data-sources.md.bsBPSQ6R.js b/assets/modeling_model-components_data-sources.md.bsBPSQ6R.js
new file mode 100644
index 000000000..db22bd0e3
--- /dev/null
+++ b/assets/modeling_model-components_data-sources.md.bsBPSQ6R.js
@@ -0,0 +1,111 @@
+import{_ as p,D as r,o as c,c as i,I as a,w as l,R as n,k as s,a as e}from"./chunks/framework.g9eZ-ZSs.js";const X=JSON.parse('{"title":"Data Sources","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/data-sources.md","filePath":"modeling/model-components/data-sources.md"}'),D={name:"modeling/model-components/data-sources.md"},d=n(`
In Coalesce, all data that is retrieved from your database through the generated controllers is done so by a data source. These data sources control what data gets loaded and how it gets loaded. By default, there is a single generic data source that serves all data for your models in a generic way that fits many of the most common use cases - the Standard Data Source.
In addition to this standard data source, Coalesce allows you to create custom data sources that provide complete control over the way data is loaded and serialized for transfer to a requesting client. These data sources are defined on a per-model basis, and you can have as many of them as you like for each model.
By default, each of your models that Coalesce exposes will expose the standard data source (IntelliTect.Coalesce.StandardDataSource<T, TContext>). This data source provides all the standard functionality one would expect - paging, sorting, searching, filtering, and so on. Each of these component pieces is implemented in one or more virtual methods, making the StandardDataSource a great place to start from when implementing your own data source. To suppress this behavior of always exposing the raw StandardDataSource, create your own custom data source and annotate it with [DefaultDataSource].
To implement your own custom data source, you simply need to define a class that implements IntelliTect.Coalesce.IDataSource<T>. To expose your data source to Coalesce, either place it as a nested class of the type T that you data source serves, or annotate it with the [Coalesce] attribute. Of course, the easiest way to create a data source that doesn't require you to re-engineer a great deal of logic would be to inherit from IntelliTect.Coalesce.StandardDataSource<T, TContext>, and then override only the parts that you need.
c#
public class Person
+{
+ [DefaultDataSource]
+ public class IncludeFamily : StandardDataSource<Person, AppDbContext>
+ {
+ public IncludeFamily(CrudContext<AppDbContext> context) : base(context) { }
+
+ public override IQueryable<Person> GetQuery(IDataSourceParameters parameters)
+ => Db.People
+ .Where(f => User.IsInRole("Admin") || f.CreatedById == User.GetUserId())
+ .Include(f => f.Parents).ThenInclude(s => s.Parents)
+ .Include(f => f.Cousins).ThenInclude(s => s.Parents);
+ }
+}
+
+[Coalesce]
+public class NamesStartingWithA : StandardDataSource<Person, AppDbContext>
+{
+ public NamesStartingWithA(CrudContext<AppDbContext> context) : base(context) { }
+
+ public override IQueryable<Person> GetQuery(IDataSourceParameters parameters)
+ => Db.People.Include(f => f.Siblings).Where(f => f.FirstName.StartsWith("A"));
+}
The structure of the IQueryable built by the various methods of StandardDataSource is used to shape and trim the structure of the DTO as it is serialized and sent out to the client. One may also override method IncludeTree GetIncludeTree(IQueryable<Person> query, IDataSourceParameters parameters) to control this explicitly. See Include Tree for more information on how this works.
WARNING
If you create a custom data source that has custom logic for securing your data, be aware that the default implementation of StandardDataSource (or your custom default implementation - see below) is still exposed unless you annotate one of your custom data sources with [DefaultDataSource]. Doing so will replace the default data source with the annotated class for your type T.
All data sources are instantiated using dependency injection and your application's IServiceProvider. As a result, you can add whatever constructor parameters you desire to your data sources as long as a value for them can be resolved from your application's services. The single parameter to the StandardDataSource is resolved in this way - the CrudContext<TContext> contains the common set of objects most commonly used, including the DbContext and the ClaimsPrincipal representing the current user.
`,12),y=s("p",null,[e("The "),s("a",{href:"/Coalesce/stacks/vue/layers/viewmodels.html#viewmodels"},"ViewModels"),e(" and "),s("a",{href:"/Coalesce/stacks/vue/layers/viewmodels.html#listviewmodels"},"ListViewModels"),e(" each have a property called "),s("code",null,"$dataSource"),e(". This property accepts an instance of a "),s("a",{href:"/Coalesce/stacks/vue/layers/models.html"},"DataSource"),e(" class generated in the "),s("a",{href:"/Coalesce/stacks/vue/layers/models.html"},"Model Layer"),e(".")],-1),u=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"Person"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/models.g'")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},", "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," Person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"DataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"IncludeFamily"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," Person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"DataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"NamesStartingWith"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")])])])],-1),C=s("p",null,[e("The "),s("a",{href:"/Coalesce/stacks/ko/client/view-model.html"},"TypeScript ViewModels"),e(" and "),s("a",{href:"/Coalesce/stacks/ko/client/list-view-model.html"},"TypeScript ListViewModels"),e(" each have a property called "),s("code",null,"dataSource"),e(". These properties accept an instance of a "),s("code",null,"Coalesce.DataSource"),e(". Generated classes that satisfy this relationship for all the data sources that were defined in C# may be found in the "),s("code",null,"dataSources"),e(" property on an instance of a ViewModel or ListViewModel, or in "),s("code",null,"ListViewModels.DataSources")],-1),h=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"IncludeFamily"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"NamesStartingWith"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"();")])])])],-1),m=n(`
All methods on IDataSource<T> take a parameter that contains all the client-specified parameters for things paging, searching, sorting, and filtering information. Almost all virtual methods on StandardDataSource are also passed the relevant set of parameters.
On any data source that you create, you may add additional properties annotated with [Coalesce] that will then be exposed as parameters to the client. These property parameters are currently restricted to primitives (numeric types, strings) and dates (DateTime, DateTimeOffset). Property parameter primitives may be expanded to allow for more types in the future.
You can setup TypeScript List ViewModels to automatically reload from the server when data source parameters change:
`,7),f=s("p",null,[e("To automatically reload a "),s("a",{href:"/Coalesce/stacks/vue/layers/viewmodels.html"},"ListViewModel"),e(" when data source parameters change, simply use the list's "),s("code",null,"$useAutoLoad"),e(" or "),s("code",null,"$startAutoLoad"),e(" function:")],-1),g=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"Person"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/models.g'"),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'"),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," Person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"DataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"NamesStartingWith")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$useAutoLoad"),s("span",{style:{color:"#D4D4D4"}},"(); "),s("span",{style:{color:"#6A9955"}},"// When using options API, use $startAutoLoad(this)")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Trigger a reload:")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"startsWith"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Jo"'),s("span",{style:{color:"#D4D4D4"}},";")])])])],-1),b=s("p",null,"The properties created on the TypeScript objects are observables so they may be bound to directly. In order to automatically reload a list when a data source parameter changes, you must explicitly subscribe to it:",-1),A=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"NamesStartingWith"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"startsWith"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#CE9178"}},'"Jo"'),s("span",{style:{color:"#D4D4D4"}},");")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"subscribe"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"); "),s("span",{style:{color:"#6A9955"}},"// Enables automatic reloading.")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"();")])])])],-1),v=n('
The standard data sources, IntelliTect.Coalesce.StandardDataSource<T> and its EntityFramework-supporting sibling IntelliTect.Coalesce.StandardDataSource<T, TContext>, contain a significant number of properties and methods that can be utilized and/or overridden at your leisure.
When an object or list of objects is requested, the default behavior of the the StandardDataSource is to load all of the immediate relationships of the object (parent objects and child collections), as well as the far side of many-to-many relationships. This is performed in StandardDataSource.GetQuery(), so in order to suppress this behavior in a custom data source, don't build you query off of base.GetQuery(), but instead start directly from the DbSet for your entity when building your custom query.
Clients can suppress this per-request by setting includes = "none" on your TypeScript ViewModel or ListViewModel, but note this is not a security mechanism and should only be used to reduce payload size or improve response time.
On the server, you can suppress this behavior by placing [Read(NoAutoInclude = true)] on either an entire class (affecting all navigation properties of that type), or on specific navigation properties. When placed on a entity class that holds sensitive data, this can help ensure you don't accidentally leak records due to forgetting to customize the data sources of the types whose navigation properties reference your sensitive entity.
The following properties are available for use on the StandardDataSource any any derived instances.
',8),S=s("p",null,"The object passed to the constructor that contains the set of objects needed by the standard data source, and those that are most likely to be used in custom implementations.",-1),E=s("p",null,[e("An instance of the DbContext that contains a "),s("code",null,"DbSet"),e(" for the entity served by the data source.")],-1),T=s("p",null,"The user making the current request.",-1),_=s("p",null,"The max number of search terms to process when interpreting a search term word-by-word. Override by setting a value in the constructor.",-1),w=s("p",null,"The page size to use if none is specified by the client. Override by setting a value in the constructor.",-1),F=n(`
The maximum page size that will be served. By default, client-specified page sizes will be clamped to this value. Override by setting a value in the constructor.
The standard data source contains 19 different methods which can be overridden in your derived class to control its behavior.
These methods often call one another, so overriding one method may cause some other method to no longer be called. The hierarchy of method calls, ignoring any logic or conditions contained within, is as follows:
All of the methods outlined above can be overridden. A description of each of the non-interface inner methods is as follows:
`,7),I=n('
The method is the one that you will most commonly be override in order to implement custom query logic. The default implementation of GetQueryAsync simply calls GetQuery - be aware of this in cases of complex overrides/inheritance. From this method, you could:
Specify additional query filtering such as row-level security or soft-delete logic. Or, restrict the data source entirely for users or whole roles by returning an empty query.
Include additional data using EF's .Include() and .ThenInclude().
Add additional edges to the serialized object graph using Coalesce's .IncludedSeparately() and .ThenIncluded().
Note
When GetQuery is overridden, the Default Loading Behavior is overridden as well. To restore this behavior, use the IQueryable<T>.IncludeChildren() extension method to build your query.
',3),P=s("p",null,[e("Allows for explicitly specifying the "),s("a",{href:"/Coalesce/concepts/include-tree.html"},"Include Tree"),e(" that will be used when serializing results obtained from this data source into DTOs. By default, the query that is build up through all the other methods in the data source will be used to build the include tree.")],-1),k=s("p",null,[e("Called by other methods in the standard data source to determine whether or not EF Core async methods will be used to evaluate queries. This may be globally disabled when bugs like "),s("a",{href:"https://github.com/dotnet/SqlClient/issues/593",target:"_blank",rel:"noreferrer"},"https://github.com/dotnet/SqlClient/issues/593"),e(" are present in EF Core.")],-1),x=s("p",null,[e("A simple wrapper that calls "),s("code",null,"ApplyListPropertyFilters"),e(" and "),s("code",null,"ApplyListSearchTerm"),e(".")],-1),q=s("p",null,[e("For each value in "),s("code",null,"parameters.Filter"),e(", invoke "),s("code",null,"ApplyListPropertyFilter"),e(" to apply a filter to the query.")],-1),L=s("p",null,"Given a property and a client-provided string value, perform some filtering on that property.",-1),B=s("ul",null,[s("li",null,"Dates with a time component will be matched exactly."),s("li",null,"Dates with no time component will match any dates that fell on that day."),s("li",null,[e("Strings will match exactly unless an asterisk is found, in which case they will be matched with "),s("code",null,"string.StartsWith"),e(".")]),s("li",null,"Enums will match by string or numeric value. Multiple comma-delimited values will create a filter that will match on any of the provided values."),s("li",null,"Numeric values will match exactly. Multiple comma-delimited values will create a filter that will match on any of the provided values.")],-1),M=s("p",null,[e("Applies filters to the query based on the specified search term. See "),s("a",{href:"/Coalesce/modeling/model-components/attributes/search.html"},"[Search]"),e(" for a detailed look at how searching works in Coalesce.")],-1),Q=s("p",null,[e("If any client-specified sort orders are present, invokes "),s("code",null,"ApplyListClientSpecifiedSorting"),e(". Otherwise, invokes "),s("code",null,"ApplyListDefaultSorting"),e(".")],-1),V=s("p",null,[e("Applies sorting to the query based on sort orders specified by the client. If the client specified "),s("code",null,'"none"'),e(" as the sort field, no sorting will take place.")],-1),G=s("p",null,[e("Applies default sorting behavior to the query, including behavior defined with use of "),s("code",null,"[DefaultOrderBy]"),e(" in C# POCOs, as well as fallback sorting to "),s("code",null,'"Name"'),e(" or primary key properties.")],-1),N=s("p",null,"Applies paging to the query based on incoming parameters. Provides the actual page and pageSize that were used as out parameters.",-1),W=s("p",null,[e("Simple wrapper around invoking "),s("code",null,".Count()"),e(" on a query.")],-1),O=n('
Allows for transformation of a result set after the query has been evaluated. This will be called for both lists of items and for single items. This can be used for populating non-mapped properties on a model, or conditionally loading navigation properties using logic that depends upon the contents of each loaded record.
This method is only called immediately before mapping to a DTO; it does not affect operations that don't involve mapping to a DTO - e.g. when loading the target of a /save operation or when loading the invocation target of an instance method.
Do not use TransformResults to modify any database-mapped scalar properties, since such changes could be inadvertently persisted to the database.
',4),R=n(`
Performs trimming of the fields of the result set based on the parameters given to the data source. Can be overridden to forcibly disable this, override the behavior to always trim specific fields, or any other functionality desired.
You can, of course, create a custom base data source that all your custom implementations inherit from. But, what if you want to override the standard data source across your entire application, so that StandardDataSource<,> will never be instantiated? You can do that too!
Simply create a class that implements IEntityFrameworkDataSource<,> (the StandardDataSource<,> already does - feel free to inherit from it), then register it at application startup like so:
c#
public class MyDataSource<T, TContext> : StandardDataSource<T, TContext>
+ where T : class
+ where TContext : DbContext
+{
+ public MyDataSource(CrudContext<TContext> context) : base(context)
+ {
+ }
+
+ ...
+}
Your custom data source must have the same generic type parameters - <T, TContext>. Otherwise, the Microsoft.Extensions.DependencyInjection service provider won't know how to inject it.
`,7);function j(z,$,U,Y,J,H){const t=r("CodeTabs"),o=r("Prop");return c(),i("div",null,[d,a(t,null,{vue:l(()=>[y,u]),knockout:l(()=>[C,h]),_:1}),m,a(t,null,{vue:l(()=>[f,g]),knockout:l(()=>[b,A]),_:1}),v,a(o,{def:"CrudContext Context"}),S,a(o,{def:"TContext Db"}),E,a(o,{def:"ClaimsPrincipal User"}),T,a(o,{def:"int MaxSearchTerms"}),_,a(o,{def:"int DefaultPageSize"}),w,a(o,{def:"int MaxPageSize"}),F,a(o,{def:`IQueryable GetQuery(IDataSourceParameters parameters);
+Task> GetQueryAsync(IDataSourceParameters parameters);`}),I,a(o,{def:"IncludeTree? GetIncludeTree(IQueryable query, IDataSourceParameters parameters)"}),P,a(o,{def:"bool CanEvalQueryAsynchronously(IQueryable query)"}),k,a(o,{def:"IQueryable ApplyListFiltering(IQueryable query, IFilterParameters parameters)"}),x,a(o,{def:"IQueryable ApplyListPropertyFilters(IQueryable query, IFilterParameters parameters)"}),q,a(o,{def:"IQueryable ApplyListPropertyFilter(IQueryable query, PropertyViewModel prop, string value)"}),L,B,a(o,{def:"IQueryable ApplyListSearchTerm(IQueryable query, IFilterParameters parameters)"}),M,a(o,{def:"IQueryable ApplyListSorting(IQueryable query, IListParameters parameters)"}),Q,a(o,{def:"IQueryable ApplyListClientSpecifiedSorting(IQueryable query, IListParameters parameters)"}),V,a(o,{def:"IQueryable ApplyListDefaultSorting(IQueryable query)"}),G,a(o,{def:"IQueryable ApplyListPaging(IQueryable query, IListParameters parameters, int? totalCount, out int page, out int pageSize)"}),N,a(o,{def:"Task GetListTotalCountAsync(IQueryable query, IFilterParameters parameters)"}),W,a(o,{def:`void TransformResults(IReadOnlyList results, IDataSourceParameters parameters);
+Task TransformResultsAsync(IReadOnlyList results, IDataSourceParameters parameters);`}),O,a(o,{def:"IList TrimListFields(IList mappedResult, IListParameters parameters)"}),R])}const Z=p(D,[["render",j]]);export{X as __pageData,Z as default};
diff --git a/assets/modeling_model-components_data-sources.md.bsBPSQ6R.lean.js b/assets/modeling_model-components_data-sources.md.bsBPSQ6R.lean.js
new file mode 100644
index 000000000..3866a74c9
--- /dev/null
+++ b/assets/modeling_model-components_data-sources.md.bsBPSQ6R.lean.js
@@ -0,0 +1,31 @@
+import{_ as p,D as r,o as c,c as i,I as a,w as l,R as n,k as s,a as e}from"./chunks/framework.g9eZ-ZSs.js";const X=JSON.parse('{"title":"Data Sources","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/data-sources.md","filePath":"modeling/model-components/data-sources.md"}'),D={name:"modeling/model-components/data-sources.md"},d=n("",12),y=s("p",null,[e("The "),s("a",{href:"/Coalesce/stacks/vue/layers/viewmodels.html#viewmodels"},"ViewModels"),e(" and "),s("a",{href:"/Coalesce/stacks/vue/layers/viewmodels.html#listviewmodels"},"ListViewModels"),e(" each have a property called "),s("code",null,"$dataSource"),e(". This property accepts an instance of a "),s("a",{href:"/Coalesce/stacks/vue/layers/models.html"},"DataSource"),e(" class generated in the "),s("a",{href:"/Coalesce/stacks/vue/layers/models.html"},"Model Layer"),e(".")],-1),u=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"Person"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/models.g'")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},", "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," Person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"DataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"IncludeFamily"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," Person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"DataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"NamesStartingWith"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")])])])],-1),C=s("p",null,[e("The "),s("a",{href:"/Coalesce/stacks/ko/client/view-model.html"},"TypeScript ViewModels"),e(" and "),s("a",{href:"/Coalesce/stacks/ko/client/list-view-model.html"},"TypeScript ListViewModels"),e(" each have a property called "),s("code",null,"dataSource"),e(". These properties accept an instance of a "),s("code",null,"Coalesce.DataSource"),e(". Generated classes that satisfy this relationship for all the data sources that were defined in C# may be found in the "),s("code",null,"dataSources"),e(" property on an instance of a ViewModel or ListViewModel, or in "),s("code",null,"ListViewModels.DataSources")],-1),h=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"IncludeFamily"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"NamesStartingWith"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"();")])])])],-1),m=n("",7),f=s("p",null,[e("To automatically reload a "),s("a",{href:"/Coalesce/stacks/vue/layers/viewmodels.html"},"ListViewModel"),e(" when data source parameters change, simply use the list's "),s("code",null,"$useAutoLoad"),e(" or "),s("code",null,"$startAutoLoad"),e(" function:")],-1),g=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"Person"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/models.g'"),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'"),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#4FC1FF"}}," dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," Person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"DataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"NamesStartingWith")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$useAutoLoad"),s("span",{style:{color:"#D4D4D4"}},"(); "),s("span",{style:{color:"#6A9955"}},"// When using options API, use $startAutoLoad(this)")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Trigger a reload:")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"startsWith"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Jo"'),s("span",{style:{color:"#D4D4D4"}},";")])])])],-1),b=s("p",null,"The properties created on the TypeScript objects are observables so they may be bound to directly. In order to automatically reload a list when a data source parameter changes, you must explicitly subscribe to it:",-1),A=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"NamesStartingWith"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"startsWith"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#CE9178"}},'"Jo"'),s("span",{style:{color:"#D4D4D4"}},");")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"subscribe"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"); "),s("span",{style:{color:"#6A9955"}},"// Enables automatic reloading.")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},";")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"();")])])])],-1),v=n("",8),S=s("p",null,"The object passed to the constructor that contains the set of objects needed by the standard data source, and those that are most likely to be used in custom implementations.",-1),E=s("p",null,[e("An instance of the DbContext that contains a "),s("code",null,"DbSet"),e(" for the entity served by the data source.")],-1),T=s("p",null,"The user making the current request.",-1),_=s("p",null,"The max number of search terms to process when interpreting a search term word-by-word. Override by setting a value in the constructor.",-1),w=s("p",null,"The page size to use if none is specified by the client. Override by setting a value in the constructor.",-1),F=n("",7),I=n("",3),P=s("p",null,[e("Allows for explicitly specifying the "),s("a",{href:"/Coalesce/concepts/include-tree.html"},"Include Tree"),e(" that will be used when serializing results obtained from this data source into DTOs. By default, the query that is build up through all the other methods in the data source will be used to build the include tree.")],-1),k=s("p",null,[e("Called by other methods in the standard data source to determine whether or not EF Core async methods will be used to evaluate queries. This may be globally disabled when bugs like "),s("a",{href:"https://github.com/dotnet/SqlClient/issues/593",target:"_blank",rel:"noreferrer"},"https://github.com/dotnet/SqlClient/issues/593"),e(" are present in EF Core.")],-1),x=s("p",null,[e("A simple wrapper that calls "),s("code",null,"ApplyListPropertyFilters"),e(" and "),s("code",null,"ApplyListSearchTerm"),e(".")],-1),q=s("p",null,[e("For each value in "),s("code",null,"parameters.Filter"),e(", invoke "),s("code",null,"ApplyListPropertyFilter"),e(" to apply a filter to the query.")],-1),L=s("p",null,"Given a property and a client-provided string value, perform some filtering on that property.",-1),B=s("ul",null,[s("li",null,"Dates with a time component will be matched exactly."),s("li",null,"Dates with no time component will match any dates that fell on that day."),s("li",null,[e("Strings will match exactly unless an asterisk is found, in which case they will be matched with "),s("code",null,"string.StartsWith"),e(".")]),s("li",null,"Enums will match by string or numeric value. Multiple comma-delimited values will create a filter that will match on any of the provided values."),s("li",null,"Numeric values will match exactly. Multiple comma-delimited values will create a filter that will match on any of the provided values.")],-1),M=s("p",null,[e("Applies filters to the query based on the specified search term. See "),s("a",{href:"/Coalesce/modeling/model-components/attributes/search.html"},"[Search]"),e(" for a detailed look at how searching works in Coalesce.")],-1),Q=s("p",null,[e("If any client-specified sort orders are present, invokes "),s("code",null,"ApplyListClientSpecifiedSorting"),e(". Otherwise, invokes "),s("code",null,"ApplyListDefaultSorting"),e(".")],-1),V=s("p",null,[e("Applies sorting to the query based on sort orders specified by the client. If the client specified "),s("code",null,'"none"'),e(" as the sort field, no sorting will take place.")],-1),G=s("p",null,[e("Applies default sorting behavior to the query, including behavior defined with use of "),s("code",null,"[DefaultOrderBy]"),e(" in C# POCOs, as well as fallback sorting to "),s("code",null,'"Name"'),e(" or primary key properties.")],-1),N=s("p",null,"Applies paging to the query based on incoming parameters. Provides the actual page and pageSize that were used as out parameters.",-1),W=s("p",null,[e("Simple wrapper around invoking "),s("code",null,".Count()"),e(" on a query.")],-1),O=n("",4),R=n("",7);function j(z,$,U,Y,J,H){const t=r("CodeTabs"),o=r("Prop");return c(),i("div",null,[d,a(t,null,{vue:l(()=>[y,u]),knockout:l(()=>[C,h]),_:1}),m,a(t,null,{vue:l(()=>[f,g]),knockout:l(()=>[b,A]),_:1}),v,a(o,{def:"CrudContext Context"}),S,a(o,{def:"TContext Db"}),E,a(o,{def:"ClaimsPrincipal User"}),T,a(o,{def:"int MaxSearchTerms"}),_,a(o,{def:"int DefaultPageSize"}),w,a(o,{def:"int MaxPageSize"}),F,a(o,{def:`IQueryable GetQuery(IDataSourceParameters parameters);
+Task> GetQueryAsync(IDataSourceParameters parameters);`}),I,a(o,{def:"IncludeTree? GetIncludeTree(IQueryable query, IDataSourceParameters parameters)"}),P,a(o,{def:"bool CanEvalQueryAsynchronously(IQueryable query)"}),k,a(o,{def:"IQueryable ApplyListFiltering(IQueryable query, IFilterParameters parameters)"}),x,a(o,{def:"IQueryable ApplyListPropertyFilters(IQueryable query, IFilterParameters parameters)"}),q,a(o,{def:"IQueryable ApplyListPropertyFilter(IQueryable query, PropertyViewModel prop, string value)"}),L,B,a(o,{def:"IQueryable ApplyListSearchTerm(IQueryable query, IFilterParameters parameters)"}),M,a(o,{def:"IQueryable ApplyListSorting(IQueryable query, IListParameters parameters)"}),Q,a(o,{def:"IQueryable ApplyListClientSpecifiedSorting(IQueryable query, IListParameters parameters)"}),V,a(o,{def:"IQueryable ApplyListDefaultSorting(IQueryable query)"}),G,a(o,{def:"IQueryable ApplyListPaging(IQueryable query, IListParameters parameters, int? totalCount, out int page, out int pageSize)"}),N,a(o,{def:"Task GetListTotalCountAsync(IQueryable query, IFilterParameters parameters)"}),W,a(o,{def:`void TransformResults(IReadOnlyList results, IDataSourceParameters parameters);
+Task TransformResultsAsync(IReadOnlyList results, IDataSourceParameters parameters);`}),O,a(o,{def:"IList TrimListFields(IList mappedResult, IListParameters parameters)"}),R])}const Z=p(D,[["render",j]]);export{X as __pageData,Z as default};
diff --git a/assets/modeling_model-components_methods.md._iwSjDHF.js b/assets/modeling_model-components_methods.md._iwSjDHF.js
new file mode 100644
index 000000000..95ff3ef51
--- /dev/null
+++ b/assets/modeling_model-components_methods.md._iwSjDHF.js
@@ -0,0 +1,134 @@
+import{_ as l,D as t,o as p,c as r,I as c,w as a,R as n,k as s,a as e}from"./chunks/framework.g9eZ-ZSs.js";const B=JSON.parse('{"title":"Methods","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/methods.md","filePath":"modeling/model-components/methods.md"}'),i={name:"modeling/model-components/methods.md"},D=n(`
Any public methods annotated with the [Coalesce] attribute that are placed on your model classes will have API endpoints and Typescript generated by Coalesce. Both instance methods and static methods are supported. Additionally, any instance methods on Services will also have API endpoints and TypeScript generated.
These custom methods allow you to implement any custom server-side functionality in your Coalesce application that falls outside of the standard CRUD functions that are generated for your entities.
Instance Methods can be declared on your Entity classes. For example:
c#
public class User
+{
+ public int UserId { get; set; }
+
+ public string Email { get; set; }
+
+ [Coalesce]
+ public async Task<ItemResult> SendMessage(
+ [Inject] SmtpClient client,
+ ClaimsPrincipal sender,
+ string message
+ ) {
+ if (string.IsNullOrWhitespace(Email)) return "Recipient has no email";
+ if (string.IsNullOrWhitespace(message)) return "Message is required";
+
+ await client.SendMailAsync(new MailMessage(
+ from: sender.GetEmailAddress(),
+ to: Email,
+ subject: "Message from MyApp",
+ body: message
+ ));
+ return true;
+ }
+}
When an instance method is invoked, the target model instance will be loaded using the data source specified by an attribute [LoadFromDataSource(typeof(MyDataSource))] if present. Otherwise, the model instance will be loaded using the default data source for the model's type. If you have a Custom Data Source annotated with [DefaultDataSource], that data source will be used. Otherwise, the Standard Data Source will be used. The consequence of this is that a user cannot call a method on an instance of entity that they're not allowed to see or load.
Instance methods are generated onto the TypeScript ViewModels.
Instance methods, as opposed to static or service methods, are a good fit when implementing an action that directly acts on or depends upon a specific instance of one of your entity types. One of their biggest benefits is the automatic row-level security from data sources as described above.
Static Methods can be declared on your Entity classes. For example:
c#
public class Person
+{
+ public int PersonId { get; set; }
+
+ public string FirstName { get; set; }
+
+ [Coalesce]
+ public static ICollection<string> NamesStartingWith(
+ AppDbContext db,
+ string characters
+ ) {
+ return db.People
+ .Select(p => p.FirstName)
+ .Where(f => f.StartsWith(characters))
+ .ToList();
+ }
+}
Static methods are generated onto the TypeScript ListViewModels. All of the same members that are generated for instance methods are also generated for static methods.
Static methods are a good fit for actions that don't operate on a specific instance of an entity type, but whose functionality is still closely coupled with a specific, concrete entity type.
For example, imagine you have a File entity class. You could make a static method on that class that accepts a file as a parameter. This method would persist that file to storage and then save a new entity to the database. You would then disable Create on that entity, since the default /save endpoint cannot accept file uploads.
Or, imagine an Invoice class. You might make a static method that returns a summary of sales information for a given time range. Since this summarization would be performing aggregate functions against your Invoice entities and is therefore tightly coupled to Invoices, a static method would be suitable.
Services are a catch-all feature and can be used for almost any conceivable purpose in Coalesce to implement custom functionality that needs to be invoked by your front-end app.
However, there are some reasons why you might not want to use a service:
If the method logically operates on a single entity instance, and/or if using an instance method would let you utilize the row-level security already implemented by one of your data sources to authorize who can invoke the method.
If the service would only have one or two methods and would logically make sense as a static or instance method. In other words, if adding a new service class would be detrimental to the organization of your codebase and create "file sprawl".
On the other hand, services have some benefits that instance and static methods cannot provide:
Coalesce Services can be declared with an interface, rather than a concrete type, allowing for their implementation to be substituted more easily. For example, a service providing an external integration that you want to mock or stub during automated testing and/or local development.
The following parameters can be added to your methods:
Type
Description
Primitives, Dates, and other Scalars
Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, DateTime, DateTimeOffset), and their nullable variants, are accepted as parameters to be passed from the client to the method call.
When invoking the method on the client, the object's properties will only be serialized one level deep. If an entity model parameter has additional child object properties, they will not be included in the invocation of the method - only the object's primitive & date properties will be deserialized from the client.
Unlike entity model parameters, external type parameters will be serialized and sent by the client to an arbitrarily deep level, excluding any entity model properties that may be nested inside an external type.
Files
Methods can accept file uploads by using a parameter of type IntelliTect.Coalesce.Models.IFile (or any derived type, like IntelliTect.Coalesce.Models.File).
Deprecated. If you need to return an Include Tree to shape the serialization of the method's return value, you should use an ItemResult<T> return value and populate the IncludeTree property on the ItemResult object.
You can return virtually anything from these methods:
Type
Description
Primitives, Dates, and other Scalars
Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, DateTime, DateTimeOffset), and their nullable variants, may be returned from methods.
Any External Types you define may also be returned from a method.
When returning custom types from methods, be careful of the types of their properties. Coalesce will recursively discover and generate code for all public properties of your External Types. If you accidentally include a type that you do not own, these generated types could get out of hand extremely quickly.
Mark any properties you don't want generated with the [InternalUse] attribute, or give them a non-public access modifier. Whenever possible, don't return types that you don't own or control.
Collections of any of the above valid return types above are also valid return types. IEnumerables are useful for generator functions using yield. ICollection is highly suggested over IEnumerable whenever appropriate, though.
Queryables of the valid return types above are valid return types. The query will be evaluated, and Coalesce will attempt to pull an Include Tree from the queryable to shape the response.
When Include Tree functionality is needed to shape the response but an IQueryable<> return type is not feasible, an ItemResult return value with an IncludeTree set on it will do the trick as well.
Methods can return file downloads using type IntelliTect.Coalesce.Models.IFile (or any derived type, like IntelliTect.Coalesce.Models.File).
Please see the File Downloads section below for more details
ItemResult<T>, ItemResult, ListResult<T>
An IntelliTect.Coalesce.Models.ItemResult<T> of any of the valid return types above, including collections, is valid, as well as its non-generic variant ItemResult, and its list variant ListResult<T>.
Use an ItemResult whenever you might need to signal failure and return an error message from a custom method. The WasSuccessful and Message properties on the result object will be sent along to the client to indicate success or failure of the method. The type T will be mapped to the appropriate DTO object before being serialized as normal.
An Include Tree can be set on the object's IncludeTree parameter to shape the serialization of the method's returned value.
You can implement role-based security on a method by placing the [Execute] on the method. Placing this attribute on the method with no roles specified will simply require that the calling user be authenticated.
Security for instance methods is also controlled by the data source that loads the instance - if the data source can't provide an instance of the requested model, the method won't be executed.
See the Security page to read more about custom method security, as well as all other security mechanisms in Coalesce.
Methods can be annotated with attributes to control API exposure and TypeScript generation. The following attributes are available for model methods. General annotations can be found on the Attributes page.
The [Coalesce] attribute causes the method to be exposed via a generated API controller. This is not needed for methods defined on an interface marked with [Service] - Coalesce assumes that all methods on the interface are intended to be exposed. If this is not desired, create a new, more restricted interface with only the desired methods to be exposed.
The [ControllerAction] attribute controls how this method is exposed via HTTP. Can be used to customize the HTTP method/verb for the method, as well as caching behavior.
The [Execute] attribute specifies which roles can execute this method from the generated API controller. Additional security restrictions that cannot be implemented with roles should be enforced with custom code in the method's implementation.
The [LoadFromDataSource] attribute specifies that the targeted model instance method should load the instance it is called on from the specified data source when invoked from an API endpoint. If not defined, the model's default data source is used.
Coalesce supports exposing file downloads via custom methods. Simply return a IntelliTect.Coalesce.Models.IFile (or any derived type, like IntelliTect.Coalesce.Models.File), or an ItemResult<> of such.
There are a few conveniences for easily consuming downloaded files from your custom pages.
`,58),d=s("p",null,[e("The "),s("a",{href:"/Coalesce/stacks/vue/layers/api-clients.html#api-callers"},"API Callers"),e(" have a property "),s("code",null,"url"),e(". This can be provided directly to your HTML template, with the browser invoking the endpoint automatically.")],-1),y=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")])])])],-1),h=s("div",{class:"language-html"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"html"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#9CDCFE"}}," :src"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"downloadPicture.url"'),s("span",{style:{color:"#808080"}},">")])])])],-1),C=s("hr",null,null,-1),u=s("p",null,[e("Alternatively, the "),s("a",{href:"/Coalesce/stacks/vue/layers/api-clients.html#api-callers"},"API Callers"),e(" for file-returning methods have a method "),s("code",null,"getResultObjectUrl(vue)"),e(". If the method was invoked programmatically (i.e. via "),s("code",null,"caller()"),e(", "),s("code",null,"caller.invoke()"),e(", or "),s("code",null,"caller.invokeWithArgs()"),e("), this method returns an "),s("a",{href:"https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL",target:"_blank",rel:"noreferrer"},"Object URL"),e(" that can be set as the "),s("code",null,"src"),e(" of an "),s("code",null,"image"),e(" or "),s("code",null,"video"),e(" HTML tag.")],-1),m=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"downloadPicture"),s("span",{style:{color:"#D4D4D4"}},"();")])])])],-1),b=s("div",{class:"language-html"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"html"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#9CDCFE"}}," :src"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"downloadPicture.getResultObjectUrl()"'),s("span",{style:{color:"#808080"}},">")])])])],-1),g=s("p",null,[e("The "),s("a",{href:"/Coalesce/stacks/ko/client/methods.html"},"TypeScript Method Objects"),e(" for HTTP GET methods have a property "),s("code",null,"url"),e(". This can be provided directly to your HTML, with the browser invoking the endpoint as normal.")],-1),f=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")])])])],-1),v=s("div",{class:"language-html"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"html"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#9CDCFE"}}," data-bind"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"attr: {src: downloadPicture.url }"'),s("span",{style:{color:"#808080"}},">")])])])],-1),E=s("hr",null,null,-1),w=s("p",null,[e("Alternatively, the "),s("a",{href:"/Coalesce/stacks/ko/client/methods.html"},"TypeScript Method Objects"),e(" for file-returning methods have a property "),s("code",null,"resultObjectUrl"),e(". If the method is invoked programmatically (i.e. via "),s("code",null,".invoke()"),e(" or "),s("code",null,".invokeWithArgs()"),e("), this property contains an "),s("a",{href:"https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL",target:"_blank",rel:"noreferrer"},"Object URL"),e(" that can be set as the "),s("code",null,"src"),e(" of an "),s("code",null,"image"),e(" or "),s("code",null,"video"),e(" HTML tag.")],-1),A=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},", () "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"downloadPicture"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"invoke"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")])])])],-1),F=s("div",{class:"language-html"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"html"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#9CDCFE"}}," data-bind"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"attr: {src: downloadPicture.resultObjectUrl }"'),s("span",{style:{color:"#808080"}},">")])])])],-1),T=n(`
When storing large byte[] objects in your EF models, it is important that these are never loaded unless necessary. Loading these can cause significant garbage collector churn, or even bring your app to a halt. To achieve this with EF, you can either utilize Table Splitting, or you can use an entire dedicated table that only contains a primary key and the binary content, and nothing else.
WARNING
Storing large binary objects in relational databases comes with significant drawbacks. For large-volume cloud solutions, it is much more costly than dedicated cloud-native file storage like Azure Storage or S3. Also of note is that the larger a database is, the more difficult its backup process becomes.
For files that are stored in your database, Coalesce supports a pattern that allows the file to be streamed directly to the HTTP response without needing to allocate a chunk of memory for the whole file at once. Simply pass an EF IQueryable<byte[]> to the constructor of IntelliTect.Coalesce.Models.File. This implementation, however, is specific to the underlying EF database provider. Currently, only SQL Server and SQLite are supported. Please open a Github issue to request support for other providers. An example of this mechanism is included in the DownloadAttachment method in the code sample below.
The following is an example of utilizing Table Splitting for database-stored files. Generally speaking, metadata about the file should be stored on the "main" entity, and only the bytes of the content should be split into a separate entity.
c#
public class AppDbContext : DbContext
+{
+ public DbSet<Case> Cases { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder
+ .Entity<Case>()
+ .ToTable("Cases")
+ .HasOne(c => c.AttachmentContent)
+ .WithOne()
+ .HasForeignKey<CaseAttachmentContent>(c => c.CaseId);
+ modelBuilder
+ .Entity<CaseAttachmentContent>()
+ .ToTable("Cases")
+ .HasKey(d => d.CaseId);
+ }
+}
+
+public class Case
+{
+ public int CaseId { get; set; }
+
+ [Read]
+ public string AttachmentName { get; set; }
+
+ [Read]
+ public long AttachmentSize { get; set; }
+
+ [Read]
+ public string AttachmentType { get; set; }
+
+ [Read, MaxLength(32)] // Adjust max length based on chosen hash algorithm.
+ public byte[] AttachmentHash { get; set; } // Could also be a base64 string if so desired.
+
+ [InternalUse]
+ public CaseAttachmentContent AttachmentContent { get; set; } = new();
+
+ [Coalesce]
+ public async Task UploadAttachment(AppDbContext db, IFile file)
+ {
+ if (file.Content == null) return;
+
+ var content = new byte[file.Length];
+ await file.Content.ReadAsync(content.AsMemory());
+
+ AttachmentContent = new () { CaseId = CaseId, Content = content };
+ AttachmentName = file.Name;
+ AttachmentSize = file.Length;
+ AttachmentType = file.ContentType;
+ AttachmentHash = SHA256.HashData(content);
+ }
+
+ [Coalesce]
+ [ControllerAction(HttpMethod.Get, VaryByProperty = nameof(AttachmentHash))]
+ public IFile DownloadAttachment(AppDbContext db)
+ {
+ return new IntelliTect.Coalesce.Models.File(db.Cases
+ .Where(c => c.CaseId == this.CaseId)
+ .Select(c => c.AttachmentContent.Content)
+ )
+ {
+ Name = AttachmentName,
+ ContentType = AttachmentType,
+ };
+ }
+}
+
+public class CaseAttachmentContent
+{
+ public int CaseId { get; set; }
+
+ [Required]
+ public byte[] Content { get; set; }
+}
For any other storage mechanism, implementations are similar to the database storage approach above. However, instead of table splitting or using a whole separate table, the file contents are simply stored elsewhere. Continue storing metadata about the file on the primary entity, and implement upload/download methods as desired that wrap the storage provider.
For downloads, prefer directly providing the underlying Stream to the IFile versus wrapping a byte[] in a MemoryStream. This will reduce server memory usage and garbage collector churn.
For cloud storage providers where complex security logic is not needed, consider having clients consume the URL of the cloud resource directly rather than passing the file content through your own server.
`,10);function k(I,S,M,_,q,x){const o=t("CodeTabs");return p(),r("div",null,[D,c(o,null,{vue:a(()=>[d,y,h,C,u,m,b]),knockout:a(()=>[g,f,v,E,w,A,F]),_:1}),T])}const R=l(i,[["render",k]]);export{B as __pageData,R as default};
diff --git a/assets/modeling_model-components_methods.md._iwSjDHF.lean.js b/assets/modeling_model-components_methods.md._iwSjDHF.lean.js
new file mode 100644
index 000000000..32cf23d07
--- /dev/null
+++ b/assets/modeling_model-components_methods.md._iwSjDHF.lean.js
@@ -0,0 +1,12 @@
+import{_ as l,D as t,o as p,c as r,I as c,w as a,R as n,k as s,a as e}from"./chunks/framework.g9eZ-ZSs.js";const B=JSON.parse('{"title":"Methods","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/methods.md","filePath":"modeling/model-components/methods.md"}'),i={name:"modeling/model-components/methods.md"},D=n("",58),d=s("p",null,[e("The "),s("a",{href:"/Coalesce/stacks/vue/layers/api-clients.html#api-callers"},"API Callers"),e(" have a property "),s("code",null,"url"),e(". This can be provided directly to your HTML template, with the browser invoking the endpoint automatically.")],-1),y=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")])])])],-1),h=s("div",{class:"language-html"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"html"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#9CDCFE"}}," :src"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"downloadPicture.url"'),s("span",{style:{color:"#808080"}},">")])])])],-1),C=s("hr",null,null,-1),u=s("p",null,[e("Alternatively, the "),s("a",{href:"/Coalesce/stacks/vue/layers/api-clients.html#api-callers"},"API Callers"),e(" for file-returning methods have a method "),s("code",null,"getResultObjectUrl(vue)"),e(". If the method was invoked programmatically (i.e. via "),s("code",null,"caller()"),e(", "),s("code",null,"caller.invoke()"),e(", or "),s("code",null,"caller.invokeWithArgs()"),e("), this method returns an "),s("a",{href:"https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL",target:"_blank",rel:"noreferrer"},"Object URL"),e(" that can be set as the "),s("code",null,"src"),e(" of an "),s("code",null,"image"),e(" or "),s("code",null,"video"),e(" HTML tag.")],-1),m=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#CE9178"}}," '@/viewmodels.g'")]),e(`
+`),s("span",{class:"line"}),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#DCDCAA"}}," PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"downloadPicture"),s("span",{style:{color:"#D4D4D4"}},"();")])])])],-1),b=s("div",{class:"language-html"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"html"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#9CDCFE"}}," :src"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"downloadPicture.getResultObjectUrl()"'),s("span",{style:{color:"#808080"}},">")])])])],-1),g=s("p",null,[e("The "),s("a",{href:"/Coalesce/stacks/ko/client/methods.html"},"TypeScript Method Objects"),e(" for HTTP GET methods have a property "),s("code",null,"url"),e(". This can be provided directly to your HTML, with the browser invoking the endpoint as normal.")],-1),f=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")])])])],-1),v=s("div",{class:"language-html"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"html"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#9CDCFE"}}," data-bind"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"attr: {src: downloadPicture.url }"'),s("span",{style:{color:"#808080"}},">")])])])],-1),E=s("hr",null,null,-1),w=s("p",null,[e("Alternatively, the "),s("a",{href:"/Coalesce/stacks/ko/client/methods.html"},"TypeScript Method Objects"),e(" for file-returning methods have a property "),s("code",null,"resultObjectUrl"),e(". If the method is invoked programmatically (i.e. via "),s("code",null,".invoke()"),e(" or "),s("code",null,".invokeWithArgs()"),e("), this property contains an "),s("a",{href:"https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL",target:"_blank",rel:"noreferrer"},"Object URL"),e(" that can be set as the "),s("code",null,"src"),e(" of an "),s("code",null,"image"),e(" or "),s("code",null,"video"),e(" HTML tag.")],-1),A=s("div",{class:"language-ts"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"ts"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#9CDCFE"}}," ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},", () "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}}," viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"downloadPicture"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"invoke"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(`
+`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")])])])],-1),F=s("div",{class:"language-html"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"},"html"),s("pre",{class:"shiki dark-plus vp-code"},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#9CDCFE"}}," data-bind"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"attr: {src: downloadPicture.resultObjectUrl }"'),s("span",{style:{color:"#808080"}},">")])])])],-1),T=n("",10);function k(I,S,M,_,q,x){const o=t("CodeTabs");return p(),r("div",null,[D,c(o,null,{vue:a(()=>[d,y,h,C,u,m,b]),knockout:a(()=>[g,f,v,E,w,A,F]),_:1}),T])}const R=l(i,[["render",k]]);export{B as __pageData,R as default};
diff --git a/assets/modeling_model-components_properties.md.gx6u8T0D.js b/assets/modeling_model-components_properties.md.gx6u8T0D.js
new file mode 100644
index 000000000..d17d69f2e
--- /dev/null
+++ b/assets/modeling_model-components_properties.md.gx6u8T0D.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as o,R as a}from"./chunks/framework.g9eZ-ZSs.js";const y=JSON.parse('{"title":"Properties","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/properties.md","filePath":"modeling/model-components/properties.md"}'),r={name:"modeling/model-components/properties.md"},i=a('
Models in a Coalesce application are just EF Core POCOs. The properties defined on your models should fit within the constraints of EF Core.
Coalesce currently has a few more restrictions than what EF Core allows, but hopefully over time some of these restrictions can be relaxed as Coalesce grows in capability.
To work with Coalesce, your model must have a single property for a primary key. By convention, this property should be named the same as your model class with Id appended to that name, but you can also annotate a property with [Key] or name it exactly "Id" to denote it as the primary key.
While a foreign key may be declared on your model using only the EF OnModuleBuilding method to specify its purpose, Coalesce won't know what the property is a key for. Therefore, foreign key properties should always be accompanied by a reference navigation property, and vice versa.
In cases where the foreign key is not named after the navigation property with "Id" appended, the [ForeignKeyAttribute] may be used on either the key or the navigation property to denote the other property of the pair, in accordance with the recommendations set forth by EF Core's Modeling Guidelines.
Collection navigation properties can be used in a straightforward manner. In the event where the inverse property on the other side of the relationship cannot be determined, [InversePropertyAttribute] will need to be used. EF Core provides documentation on how to use this attribute. Errors will be displayed at generation time if an inverse property cannot be determined without the attribute. We recommend recommended that you declare the type of collection navigation properties as ICollection<T>.
Properties of a type that are not on your DbContext will also have corresponding properties generated on the TypeScript ViewModels typed as TypeScript External ViewModels, and the values of such properties will be sent with the object to the client when requested. Properties of this type will also be sent back to the server by the client when they are encountered (currently supported by the Vue Stack only).
Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, DateTime, DateTimeOffset), and their nullable variants, are all supported as model properties.
Any property that only has a getter will also have a corresponding property generated in the TypeScript ViewModels, but won't be sent back to the server during any save actions.
If such a property is defined as an auto-property, the [NotMapped] attribute should be used to prevent EF Core from attempting to map such a property to your database.
Properties on Entity Models that use an init accessor rather than a set accessor will be implicitly treated as required, and can also only have a value provided when the entity is created for the first time. Any values provided during save actions for init-only properties when updating an existing entity will be ignored.
Property values received by the server from the client will be ignored if rejected by any property-level Security. This security is implemented in the Generated C# DTOs.
While Coalesce does not do anything special for the [NotMapped] attribute, it is still an important attribute to keep in mind while building your model, as it prevents EF Core from doing anything with the property.
',32),n=[i];function s(l,p,d,c,h,m){return t(),o("div",null,n)}const f=e(r,[["render",s]]);export{y as __pageData,f as default};
diff --git a/assets/modeling_model-components_properties.md.gx6u8T0D.lean.js b/assets/modeling_model-components_properties.md.gx6u8T0D.lean.js
new file mode 100644
index 000000000..45ad47681
--- /dev/null
+++ b/assets/modeling_model-components_properties.md.gx6u8T0D.lean.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as o,R as a}from"./chunks/framework.g9eZ-ZSs.js";const y=JSON.parse('{"title":"Properties","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-components/properties.md","filePath":"modeling/model-components/properties.md"}'),r={name:"modeling/model-components/properties.md"},i=a("",32),n=[i];function s(l,p,d,c,h,m){return t(),o("div",null,n)}const f=e(r,[["render",s]]);export{y as __pageData,f as default};
diff --git a/assets/modeling_model-types_dtos.md.SyTF2MTh.js b/assets/modeling_model-types_dtos.md.SyTF2MTh.js
new file mode 100644
index 000000000..eed295a19
--- /dev/null
+++ b/assets/modeling_model-types_dtos.md.SyTF2MTh.js
@@ -0,0 +1,86 @@
+import{_ as s,o as a,c as n,R as e}from"./chunks/framework.g9eZ-ZSs.js";const u=JSON.parse('{"title":"Custom DTOs","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-types/dtos.md","filePath":"modeling/model-types/dtos.md"}'),o={name:"modeling/model-types/dtos.md"},l=e(`
In addition to the generated Generated C# DTOs that Coalesce will create for you, you may also create your own implementations of an IClassDto. These types are first-class citizens in Coalesce - you will get a full suite of features surrounding them as if they were entities. This includes generated API Controllers, Admin Views, and full TypeScript ViewModels and TypeScript List ViewModels.
The difference between a Custom DTO and the underlying entity that they represent is as follows:
The only time your custom DTO will be served is when it is requested directly from one of the endpoints on its generated controller, or when its type is explicitly used by a method or property of another type.
When mapping data from your database, or mapping data incoming from the client, the DTO itself must manually map all properties, since there is no corresponding Generated DTO. Attributes like [DtoIncludes] & [DtoExcludes] and property-level security through Security Attributes have no effect on custom DTOs, since those attribute only affect what get generated for Generated C# DTOs.
To create a custom DTO, define a class annotated with [Coalesce] that implements IClassDTO<T>, where T is an EF Core POCO with a corresponding DbSet<T> on a DbContext that has also been exposed with [Coalesce]. Add any Properties to it just as you would add model properties to a regular EF model. If you are not exposing a DbContext class with [Coalesce] but still wish to create a Custom DTO based upon one of its entities, you can inherit from IClassDTO<T, TContext> instead as a means of explicitly declaring the type of the DbContext.
Next, ensure that one property is annotated with [Key] so that Coalesce can know the primary key of your DTO in order to perform database lookups and keep track of your object uniquely in the client-side TypeScript.
Now, populate the required MapTo and MapFrom methods with code for mapping from and to your DTO, respectively (the methods are named with respect to the underlying entity, not the DTO). Most properties probably map one-to-one in both directions, but you probably created a DTO because you wanted some sort of custom mapping - say, mapping a collection on your entity with a comma-delimited string on the DTO. This is also the place to perform any user-based, role-based, property-level security. You can access the current user on the IMappingContext object.
c#
[Coalesce]
+public class CaseDto : IClassDto<Case>
+{
+ [Key]
+ public int CaseId { get; set; }
+
+ public string Title { get; set; }
+
+ [Read]
+ public string AssignedToName { get; set; }
+
+ public void MapTo(Case obj, IMappingContext context)
+ {
+ obj.Title = Title;
+ }
+
+ public void MapFrom(Case obj, IMappingContext context = null, IncludeTree tree = null)
+ {
+ CaseId = obj.CaseKey;
+ Title = obj.Title;
+ if (obj.AssignedTo != null)
+ {
+ AssignedToName = obj.AssignedTo.Name;
+ }
+ }
+}
WARNING
Custom DTOs do not utilize property-level Security Attributes nor [DtoIncludes] & [DtoExcludes], since these are handled in the Generated DTOs. If you need property-level security or trimming, you must write it yourself in the MapTo and MapFrom methods.
If you have any child objects on your DTO, you can invoke the mapper for some other object using the static Mapper class. Also seen in this example is how to respect the Include Tree when mapping entity types; however, respecting the IncludeTree is optional. Since this DTO is a custom type that you've written, if you're certain your use cases don't need to worry about object graph trimming, then you can ignore the IncludeTree. If you do ignore the IncludeTree, you should pass null to calls to Mapper - don't pass in the incoming IncludeTree, as this could cause unexpected results.
c#
using IntelliTect.Coalesce.Mapping;
+
+[Coalesce]
+public class CaseDto : IClassDto<Case>
+{
+ public int ProductId { get; set; }
+ public Product Product { get; set; }
+ ...
+
+ public void MapFrom(Case obj, IMappingContext context = null, IncludeTree tree = null)
+ {
+ ProductId = obj.ProductId;
+
+ if (tree == null || tree[nameof(this.Product)] != null)
+ Product = Mapper.MapToDto<Product, ProductDtoGen>(obj.Product, context, tree?[nameof(this.Product)]
+ ...
+ }
+}
When you create a custom DTO, it will use the Standard Data Source and Standard Behaviors just like any of your regular Entity Models. If you wish to override this, your custom data source and/or behaviors MUST be declared in one of the following ways:
As a nested class of the DTO. The relationship between your data source or behaviors and your DTO will be picked up automatically.
c#
[Coalesce]
+public class CaseDto : IClassDto<Case>
+{
+ [Key]
+ public int CaseId { get; set; }
+
+ public string Title { get; set; }
+
+ ...
+
+ public class MyCaseDtoSource : StandardDataSource<Case, AppDbContext>
+ {
+ ...
+ }
+}
With a [DeclaredFor] attribute that references the DTO type:
c#
[Coalesce]
+public class CaseDto : IClassDto<Case>
+{
+ [Key]
+ public int CaseId { get; set; }
+
+ public string Title { get; set; }
+
+ ...
+}
+
+[Coalesce, DeclaredFor(typeof(CaseDto))]
+public class MyCaseDtoSource : StandardDataSource<Case, AppDbContext>
+{
+ ...
+}
In addition to creating a Data Source by deriving from Standard Data Source, there also exists a class ProjectedDtoDataSource that can be used to easily perform projection from EF model types to your custom DTO types using EF query projections. ProjectedDtoDataSource inherits from Standard Data Source.
Surgical saves require DTOs on the server that are capable of determining which of their properties have been set by the model binder, as surgical saves are sent from the client by entirely omitting properties from the x-www-form-urlencoded body that is sent to the server.
The Generated C# DTOs implement the necessary logic for this; however, any Custom DTOs must have this logic manually written by you, the developer. Either implement the same pattern that can be seen in the Generated C# DTOs, or do not use surgical saves with Custom DTOs.
`,23),p=[l];function t(c,r,D,y,i,C){return a(),n("div",null,p)}const h=s(o,[["render",t]]);export{u as __pageData,h as default};
diff --git a/assets/modeling_model-types_dtos.md.SyTF2MTh.lean.js b/assets/modeling_model-types_dtos.md.SyTF2MTh.lean.js
new file mode 100644
index 000000000..9b1230932
--- /dev/null
+++ b/assets/modeling_model-types_dtos.md.SyTF2MTh.lean.js
@@ -0,0 +1 @@
+import{_ as s,o as a,c as n,R as e}from"./chunks/framework.g9eZ-ZSs.js";const u=JSON.parse('{"title":"Custom DTOs","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-types/dtos.md","filePath":"modeling/model-types/dtos.md"}'),o={name:"modeling/model-types/dtos.md"},l=e("",23),p=[l];function t(c,r,D,y,i,C){return a(),n("div",null,p)}const h=s(o,[["render",t]]);export{u as __pageData,h as default};
diff --git a/assets/modeling_model-types_entities.md.kyLYfGOY.js b/assets/modeling_model-types_entities.md.kyLYfGOY.js
new file mode 100644
index 000000000..e1306ab1e
--- /dev/null
+++ b/assets/modeling_model-types_entities.md.kyLYfGOY.js
@@ -0,0 +1,48 @@
+import{_ as s,o as a,c as e,R as n}from"./chunks/framework.g9eZ-ZSs.js";const u=JSON.parse('{"title":"Entity Models","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-types/entities.md","filePath":"modeling/model-types/entities.md"}'),o={name:"modeling/model-types/entities.md"},l=n(`
Models are the core business objects of your application - they serve as the fundamental representation of data in your application. The design of your models is very important. In Entity Framework Core (EF), data models are just Plain Old CLR Objects (POCOs).
To start building your data model that Coalesce will generate code for, follow the best practices for EF Core. Guidance on this topic is available in abundance in the Entity Framework Core documentation.
Don't worry about querying or saving data when you're just getting started - Coalesce will provide a lot of that functionality for you, and it is very easy to customize what Coalesce offers later. To get started, just build your POCOs and DbContext classes. Annotate your DbContext class with [Coalesce] so that Coalesce will discover it and generate code based off of your context for you.
Before you start building, you are highly encouraged to read the sections below. The linked pages explain in greater detail what Coalesce will build for you for each part of your data model.
Read Properties for an outline of the different types of properties that you may place on your models and the code that Coalesce will generate for each of them.
Coalesce provides a number of C# attributes that can be used to decorate your model classes and their properties in order to customize behavior, appearance, security, and more. Coalesce also supports a number of annotations from System.ComponentModel.DataAnnotations.
You can place both static and interface methods on your model classes. Any public methods annotated with [Coalesce] will have a generated API endpoint and corresponding generated TypeScript members for calling this API endpoint. Read Methods to learn more.
Once you've got a solid data model in place, its time to start customizing the way that Coalesce will read your data, as well as the way that it will handle your data when processing creates, updates, and deletes.
The method by which you can control what data the users of your application can access through Coalesce's generated APIs is by creating custom data sources. These are classes that allow complete control over the way that data is retrieved from your database and provided to clients. Read Data Sources to learn more.
Behaviors in Coalesce are to mutating data as data sources are to reading data. Defining a behaviors class for a model allows complete control over the way that Coalesce will create, update, and delete your application's data in response to requests made through its generated API. Read Behaviors to learn more.
In Coalesce, Standalone Entities are entity types that are not based on Entity Framework. These types are discovered by Coalesce by annotating them with [Coalesce, StandaloneEntity].
For these types, you must define at least one custom Data Source, and optionally a Behaviors class as well. If no behaviors are defined, the type is implicitly read-only, equivalent to turning off create/edit/delete via the Security Attributes.
To define data sources and behaviors for Standalone Entities, it is recommended you inherit from StandardDataSource<T> and StandardBehaviors<T>, respectively. For example:
c#
[Coalesce, StandaloneEntity]
+public class StandaloneExample
+{
+ public int Id { get; set; }
+
+ [Search(SearchMethod = SearchAttribute.SearchMethods.Contains), ListText]
+ public string Name { get; set; } = "";
+
+ [DefaultOrderBy]
+ public DateTimeOffset Date { get; set; }
+
+ private static int nextId = 0;
+ private static ConcurrentDictionary<int, StandaloneExample> backingStore = new ConcurrentDictionary<int, StandaloneExample>();
+
+ public class DefaultSource : StandardDataSource<StandaloneExample>
+ {
+ public DefaultSource(CrudContext context) : base(context) { }
+
+ public override Task<IQueryable<StandaloneExample>> GetQueryAsync(IDataSourceParameters parameters)
+ => Task.FromResult(backingStore.Values.AsQueryable());
+ }
+
+ public class Behaviors : StandardBehaviors<StandaloneExample>
+ {
+ public Behaviors(CrudContext context) : base(context) { }
+
+ public override Task ExecuteDeleteAsync(StandaloneExample item)
+ {
+ backingStore.TryRemove(item.Id, out _);
+ return Task.CompletedTask;
+ }
+
+ public override Task ExecuteSaveAsync(SaveKind kind, StandaloneExample? oldItem, StandaloneExample item)
+ {
+ if (kind == SaveKind.Create)
+ {
+ item.Id = Interlocked.Increment(ref nextId);
+ backingStore.TryAdd(item.Id, item);
+ }
+ else
+ {
+ backingStore.TryRemove(item.Id, out _);
+ backingStore.TryAdd(item.Id, item);
+ }
+ return Task.CompletedTask;
+ }
+ }
+}
The above example is admittedly contrived, as it is unlikely that you would be using an in-memory collection as a data persistence mechanism. A more likely real-world scenario would be to inject an interface to some other data store. Data Source and Behavior classes are instantiated using your application's service provider, so any registered service can be injected.
`,25),t=[l];function p(r,c,D,i,y,d){return a(),e("div",null,t)}const h=s(o,[["render",p]]);export{u as __pageData,h as default};
diff --git a/assets/modeling_model-types_entities.md.kyLYfGOY.lean.js b/assets/modeling_model-types_entities.md.kyLYfGOY.lean.js
new file mode 100644
index 000000000..f67e3f798
--- /dev/null
+++ b/assets/modeling_model-types_entities.md.kyLYfGOY.lean.js
@@ -0,0 +1 @@
+import{_ as s,o as a,c as e,R as n}from"./chunks/framework.g9eZ-ZSs.js";const u=JSON.parse('{"title":"Entity Models","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-types/entities.md","filePath":"modeling/model-types/entities.md"}'),o={name:"modeling/model-types/entities.md"},l=n("",25),t=[l];function p(r,c,D,i,y,d){return a(),e("div",null,t)}const h=s(o,[["render",p]]);export{u as __pageData,h as default};
diff --git a/assets/modeling_model-types_external-types.md.qdzqqDbb.js b/assets/modeling_model-types_external-types.md.qdzqqDbb.js
new file mode 100644
index 000000000..90b0afb08
--- /dev/null
+++ b/assets/modeling_model-types_external-types.md.qdzqqDbb.js
@@ -0,0 +1,41 @@
+import{_ as s,o as a,c as n,R as l}from"./chunks/framework.g9eZ-ZSs.js";const u=JSON.parse('{"title":"External Types","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-types/external-types.md","filePath":"modeling/model-types/external-types.md"}'),e={name:"modeling/model-types/external-types.md"},p=l(`
In Coalesce, any type which is connected to your data model but is not directly part of it is considered to be an "external type".
The collection of external types for a data model looks like this:
Take all of the api-served types in your data model. This includes Entity Models and Custom DTOs.
Take all of the property types, method parameters, and method return types of these types.
Any of these types which are not built-in scalar types and not one of the aforementioned api-served types are external types.
For any external type discovered, any of the property types which qualify under the above rules are also external types.
WARNING
Be careful when using types that you do not own for properties and method returns in your data model. When Coalesce generates external type ViewModels and DTOs, it will not stop until it has exhausted all paths that can be reached by following public property types and method returns.
In general, you should only expose types that you have created so that you will always have full control over them. Mark any properties you don't wish to expose with [InternalUse], or make those members non-public.
External types have slightly different behavior when undergoing serialization to be sent to the client. Unlike database-mapped types which are subject to the rules of Include Tree, external types ignore the Include Tree when being mapped to DTOs for serialization. Read External Type Caveats for a more detailed explanation of this exception.
`,15),o=[p];function t(c,r,D,y,i,C){return a(),n("div",null,o)}const h=s(e,[["render",t]]);export{u as __pageData,h as default};
diff --git a/assets/modeling_model-types_external-types.md.qdzqqDbb.lean.js b/assets/modeling_model-types_external-types.md.qdzqqDbb.lean.js
new file mode 100644
index 000000000..5127b5a87
--- /dev/null
+++ b/assets/modeling_model-types_external-types.md.qdzqqDbb.lean.js
@@ -0,0 +1 @@
+import{_ as s,o as a,c as n,R as l}from"./chunks/framework.g9eZ-ZSs.js";const u=JSON.parse('{"title":"External Types","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-types/external-types.md","filePath":"modeling/model-types/external-types.md"}'),e={name:"modeling/model-types/external-types.md"},p=l("",15),o=[p];function t(c,r,D,y,i,C){return a(),n("div",null,o)}const h=s(e,[["render",t]]);export{u as __pageData,h as default};
diff --git a/assets/modeling_model-types_services.md.CHVSzKDm.js b/assets/modeling_model-types_services.md.CHVSzKDm.js
new file mode 100644
index 000000000..ce427259e
--- /dev/null
+++ b/assets/modeling_model-types_services.md.CHVSzKDm.js
@@ -0,0 +1,27 @@
+import{_ as s,o as e,c as a,R as n}from"./chunks/framework.g9eZ-ZSs.js";const h=JSON.parse('{"title":"Services","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-types/services.md","filePath":"modeling/model-types/services.md"}'),o={name:"modeling/model-types/services.md"},l=n(`
In a Coalesce application, you are likely to end up with a need for some API endpoints that aren't closely tied with your regular data model. While you could stick Static Methods on one of your entities, do so is detrimental to the organization of your code.
Instead, Coalesce allows you to generate API Controllers and a TypeScript client from a service. A service, in this case, is nothing more than a C# class or an interface with methods on it, annotated with [Coalesce, Service]. An implementation of this class or interface must be injectable from your application's service container, so a registration in Startup.cs is needed.
The instance methods of these services work just like other custom Methods in Coalesce, with one notable distinction: Instance methods don't operate on an instance of a model, but instead on a dependency injected instance of the service.
public class WeatherService : IWeatherService
+{
+ public WeatherService(AppDbContext db)
+ {
+ this.db = db;
+ }
+
+ public WeatherData GetWeather(string zipCode)
+ {
+ // Assuming some magic HttpGet method that works as follows...
+ var response = HttpGet("http://www.example.com/api/weather/" + zipCode);
+ return response.Body.SerializeTo<WeatherData>();
+ }
+
+ public void MethodThatIsNotExposedBecauseItIsNotOnTheExposedInterface() { }
+}
And a registration:
c#
public class Startup
+{
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddCoalesce<AppDbContext>();
+ services.AddScoped<IWeatherService, WeatherService>();
+ }
+}
While it isn't required that an interface for your service exist (you can generate directly from the implementation), it is highly recommended that an interface be used. Interfaces increase testability and reduce risk of accidentally changing the signature of a published API, among other benefits.
`,15),p=[l];function t(c,r,i,D,d,y){return e(),a("div",null,p)}const m=s(o,[["render",t]]);export{h as __pageData,m as default};
diff --git a/assets/modeling_model-types_services.md.CHVSzKDm.lean.js b/assets/modeling_model-types_services.md.CHVSzKDm.lean.js
new file mode 100644
index 000000000..77ff63a5e
--- /dev/null
+++ b/assets/modeling_model-types_services.md.CHVSzKDm.lean.js
@@ -0,0 +1 @@
+import{_ as s,o as e,c as a,R as n}from"./chunks/framework.g9eZ-ZSs.js";const h=JSON.parse('{"title":"Services","description":"","frontmatter":{},"headers":[],"relativePath":"modeling/model-types/services.md","filePath":"modeling/model-types/services.md"}'),o={name:"modeling/model-types/services.md"},l=n("",15),p=[l];function t(c,r,i,D,d,y){return e(),a("div",null,p)}const m=s(o,[["render",t]]);export{h as __pageData,m as default};
diff --git a/assets/security-overview.lqdZeXXG.webp b/assets/security-overview.lqdZeXXG.webp
new file mode 100644
index 000000000..428048aba
Binary files /dev/null and b/assets/security-overview.lqdZeXXG.webp differ
diff --git a/assets/stacks_agnostic_dtos.md.8_xJeZ2y.js b/assets/stacks_agnostic_dtos.md.8_xJeZ2y.js
new file mode 100644
index 000000000..be29c7047
--- /dev/null
+++ b/assets/stacks_agnostic_dtos.md.8_xJeZ2y.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as a,R as s}from"./chunks/framework.g9eZ-ZSs.js";const f=JSON.parse('{"title":"Generated C# DTOs","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/agnostic/dtos.md","filePath":"stacks/agnostic/dtos.md"}'),o={name:"stacks/agnostic/dtos.md"},r=s('
Data Transfer Objects, or DTOs, allow for transformations of data from the data store into a format more suited for transfer and use on the client side. This often means trimming properties and flattening structures to provide a leaner over-the-wire experience. Coalesce aims to support this as seamlessly as possible.
Coalesce supports two types of DTOs:
DTOs that are automatically generated for each POCO database object. These are controlled via Attributes on the POCO. These are outlined below.
DTOs that you create with IClassDto. These are outlined at Custom DTOs.
Every class that is exposed through Coalesce's generated API will have a corresponding DTO generated for it. These DTOs are used to shuttle data back and forth to the client. They are generated classes that have nullable versions of all the properties on the POCO class.
[DtoIncludes] & [DtoExcludes] and the Includes String infrastructure can be used to indicate which properties should be transferred to the client in which cases, and Include Tree is used to dictate how these DTOs are constructed from POCOs retrieved from the database.
The [Read] and [Edit] attributes can be used to apply property-level security, which manifests as conditional logic in the mapping methods on the generated DTOs.
See the Security page to read more about property-level security, as well as all other security mechanisms in Coalesce.
',9),l=[r];function n(i,c,d,h,u,p){return t(),a("div",null,l)}const T=e(o,[["render",n]]);export{f as __pageData,T as default};
diff --git a/assets/stacks_agnostic_dtos.md.8_xJeZ2y.lean.js b/assets/stacks_agnostic_dtos.md.8_xJeZ2y.lean.js
new file mode 100644
index 000000000..65c498c28
--- /dev/null
+++ b/assets/stacks_agnostic_dtos.md.8_xJeZ2y.lean.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as a,R as s}from"./chunks/framework.g9eZ-ZSs.js";const f=JSON.parse('{"title":"Generated C# DTOs","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/agnostic/dtos.md","filePath":"stacks/agnostic/dtos.md"}'),o={name:"stacks/agnostic/dtos.md"},r=s("",9),l=[r];function n(i,c,d,h,u,p){return t(),a("div",null,l)}const T=e(o,[["render",n]]);export{f as __pageData,T as default};
diff --git a/assets/stacks_agnostic_generation.md.--UgUCa6.js b/assets/stacks_agnostic_generation.md.--UgUCa6.js
new file mode 100644
index 000000000..c1c92001f
--- /dev/null
+++ b/assets/stacks_agnostic_generation.md.--UgUCa6.js
@@ -0,0 +1,18 @@
+import{_ as e,o,c as a,R as n}from"./chunks/framework.g9eZ-ZSs.js";const f=JSON.parse('{"title":"Code Generation Overview","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/agnostic/generation.md","filePath":"stacks/agnostic/generation.md"}'),s={name:"stacks/agnostic/generation.md"},t=n(`
Coalesce's principal purpose is a code generation framework for automating the creation of the boring-but-necessary parts of a web application. Below, you find an overview of the different components of Coalesce's code generation features.
Coalesce's code generation is ran via a dotnet CLI tool, dotnet coalesce. In order to invoke this tool, you must have the appropriate references to the package that provides it in your .csproj file:
All configuration of the way that Coalesce interacts with your projects, including locating, analyzing, and producing generated code, is done in a json configuration file, coalesce.json. Read more about this file at Code Generation Configuration.
There are a couple of extra options which are only available as CLI parameters to dotnet coalesce. These options do not affect the behavior of the code generation - only the behavior of the CLI itself.
--debug - When this flag is specified when running dotnet coalesce, Coalesce will wait for a debugger to be attached to its process before starting code generation.
-v|--verbosity <level> - Set the verbosity of the output. Options are trace, debug, information, warning, error, critical, and none.
For each of your Entity Models, Custom DTOs, and Services, an API controller is created in the /Api/Generated directory of your web project. These controllers provide a number of endpoints for interacting with your data.
For each of your Entity Models, a C# DTO class is created. These classes are used to hold the data that will be serialized and sent to the client, as well as data that has been received from the client before it has been mapped back to your EF POCO class.
An overview of the legacy Knockout generated code can be found at Knockout Overview.
`,23),l=[t];function r(c,p,i,d,h,u){return o(),a("div",null,l)}const g=e(s,[["render",r]]);export{f as __pageData,g as default};
diff --git a/assets/stacks_agnostic_generation.md.--UgUCa6.lean.js b/assets/stacks_agnostic_generation.md.--UgUCa6.lean.js
new file mode 100644
index 000000000..079423dd3
--- /dev/null
+++ b/assets/stacks_agnostic_generation.md.--UgUCa6.lean.js
@@ -0,0 +1 @@
+import{_ as e,o,c as a,R as n}from"./chunks/framework.g9eZ-ZSs.js";const f=JSON.parse('{"title":"Code Generation Overview","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/agnostic/generation.md","filePath":"stacks/agnostic/generation.md"}'),s={name:"stacks/agnostic/generation.md"},t=n("",23),l=[t];function r(c,p,i,d,h,u){return o(),a("div",null,l)}const g=e(s,[["render",r]]);export{f as __pageData,g as default};
diff --git a/assets/stacks_agnostic_getting-started-modeling.md.KbmYXIa-.js b/assets/stacks_agnostic_getting-started-modeling.md.KbmYXIa-.js
new file mode 100644
index 000000000..83f156957
--- /dev/null
+++ b/assets/stacks_agnostic_getting-started-modeling.md.KbmYXIa-.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as o,R as a}from"./chunks/framework.g9eZ-ZSs.js";const m=JSON.parse('{"title":"Data Modeling","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/agnostic/getting-started-modeling.md","filePath":"stacks/agnostic/getting-started-modeling.md"}'),n={name:"stacks/agnostic/getting-started-modeling.md"},i=a('
At this point, you can open up the newly-created solution in Visual Studio and run your application. However, your application won't do much without a data model, so you will probably want to do the following before running:
Create an initial Data Model by adding EF entity classes to the data project and the corresponding DbSet<> properties to AppDbContext. You will notice that the starter project includes a single model, Widget, to start with. Feel free to change this model or remove it entirely. Read Entity Models for more information about creating a data model.
Run dotnet ef migrations add Init (Init can be any name) in the data project to create an initial database migration.
Run Coalesce's code generation by either:
Running dotnet coalesce in the web project's root directory
Running the coalesce npm script (Vue) or gulp task (Knockout) in the Task Runner Explorer
You're now at a point where you can start creating your own pages!
',4),d=[i];function r(l,c,s,p,g,u){return t(),o("div",null,d)}const _=e(n,[["render",r]]);export{m as __pageData,_ as default};
diff --git a/assets/stacks_agnostic_getting-started-modeling.md.KbmYXIa-.lean.js b/assets/stacks_agnostic_getting-started-modeling.md.KbmYXIa-.lean.js
new file mode 100644
index 000000000..91713e32a
--- /dev/null
+++ b/assets/stacks_agnostic_getting-started-modeling.md.KbmYXIa-.lean.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as o,R as a}from"./chunks/framework.g9eZ-ZSs.js";const m=JSON.parse('{"title":"Data Modeling","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/agnostic/getting-started-modeling.md","filePath":"stacks/agnostic/getting-started-modeling.md"}'),n={name:"stacks/agnostic/getting-started-modeling.md"},i=a("",4),d=[i];function r(l,c,s,p,g,u){return t(),o("div",null,d)}const _=e(n,[["render",r]]);export{m as __pageData,_ as default};
diff --git a/assets/stacks_disambiguation_external-view-model.md.m3dMwNWm.js b/assets/stacks_disambiguation_external-view-model.md.m3dMwNWm.js
new file mode 100644
index 000000000..bb5367066
--- /dev/null
+++ b/assets/stacks_disambiguation_external-view-model.md.m3dMwNWm.js
@@ -0,0 +1 @@
+import{_ as e,o as a,c as t,R as o}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"TypeScript External ViewModels","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/disambiguation/external-view-model.md","filePath":"stacks/disambiguation/external-view-model.md"}'),s={name:"stacks/disambiguation/external-view-model.md"},r=o('
This is a disambiguation page for a concept in Coalesce that is implemented differently between the available front-end stack choices. Please select your preferred stack.
The Vue stack for Coalesce does not create dedicated ViewModels for External Types. The interfaces in the Model Layer are used as the only representation of External Types on the client.
',6),l=[r];function i(n,c,d,p,h,m){return a(),t("div",null,l)}const f=e(s,[["render",i]]);export{_ as __pageData,f as default};
diff --git a/assets/stacks_disambiguation_external-view-model.md.m3dMwNWm.lean.js b/assets/stacks_disambiguation_external-view-model.md.m3dMwNWm.lean.js
new file mode 100644
index 000000000..465306a81
--- /dev/null
+++ b/assets/stacks_disambiguation_external-view-model.md.m3dMwNWm.lean.js
@@ -0,0 +1 @@
+import{_ as e,o as a,c as t,R as o}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"TypeScript External ViewModels","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/disambiguation/external-view-model.md","filePath":"stacks/disambiguation/external-view-model.md"}'),s={name:"stacks/disambiguation/external-view-model.md"},r=o("",6),l=[r];function i(n,c,d,p,h,m){return a(),t("div",null,l)}const f=e(s,[["render",i]]);export{_ as __pageData,f as default};
diff --git a/assets/stacks_disambiguation_list-view-model.md.yRpM_Wq2.js b/assets/stacks_disambiguation_list-view-model.md.yRpM_Wq2.js
new file mode 100644
index 000000000..e0d3c58ab
--- /dev/null
+++ b/assets/stacks_disambiguation_list-view-model.md.yRpM_Wq2.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as a,R as i}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"TypeScript List ViewModels","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/disambiguation/list-view-model.md","filePath":"stacks/disambiguation/list-view-model.md"}'),s={name:"stacks/disambiguation/list-view-model.md"},o=i('
This is a disambiguation page for a concept in Coalesce that is implemented differently between the available front-end stack choices. Please select your preferred stack.
',6),l=[o];function c(r,n,d,p,h,m){return t(),a("div",null,l)}const k=e(s,[["render",c]]);export{_ as __pageData,k as default};
diff --git a/assets/stacks_disambiguation_list-view-model.md.yRpM_Wq2.lean.js b/assets/stacks_disambiguation_list-view-model.md.yRpM_Wq2.lean.js
new file mode 100644
index 000000000..65be2a627
--- /dev/null
+++ b/assets/stacks_disambiguation_list-view-model.md.yRpM_Wq2.lean.js
@@ -0,0 +1 @@
+import{_ as e,o as t,c as a,R as i}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"TypeScript List ViewModels","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/disambiguation/list-view-model.md","filePath":"stacks/disambiguation/list-view-model.md"}'),s={name:"stacks/disambiguation/list-view-model.md"},o=i("",6),l=[o];function c(r,n,d,p,h,m){return t(),a("div",null,l)}const k=e(s,[["render",c]]);export{_ as __pageData,k as default};
diff --git a/assets/stacks_disambiguation_view-model.md.CJJYNLOF.js b/assets/stacks_disambiguation_view-model.md.CJJYNLOF.js
new file mode 100644
index 000000000..ff45eaeda
--- /dev/null
+++ b/assets/stacks_disambiguation_view-model.md.CJJYNLOF.js
@@ -0,0 +1 @@
+import{_ as e,o as a,c as t,R as o}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"TypeScript ViewModels","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/disambiguation/view-model.md","filePath":"stacks/disambiguation/view-model.md"}'),i={name:"stacks/disambiguation/view-model.md"},s=o('
This is a disambiguation page for a concept in Coalesce that is implemented differently between the available front-end stack choices. Please select your preferred stack.
',6),c=[s];function r(l,n,d,p,h,m){return a(),t("div",null,c)}const k=e(i,[["render",r]]);export{_ as __pageData,k as default};
diff --git a/assets/stacks_disambiguation_view-model.md.CJJYNLOF.lean.js b/assets/stacks_disambiguation_view-model.md.CJJYNLOF.lean.js
new file mode 100644
index 000000000..ab1cfffe8
--- /dev/null
+++ b/assets/stacks_disambiguation_view-model.md.CJJYNLOF.lean.js
@@ -0,0 +1 @@
+import{_ as e,o as a,c as t,R as o}from"./chunks/framework.g9eZ-ZSs.js";const _=JSON.parse('{"title":"TypeScript ViewModels","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/disambiguation/view-model.md","filePath":"stacks/disambiguation/view-model.md"}'),i={name:"stacks/disambiguation/view-model.md"},s=o("",6),c=[s];function r(l,n,d,p,h,m){return a(),t("div",null,c)}const k=e(i,[["render",r]]);export{_ as __pageData,k as default};
diff --git a/assets/stacks_ko_client_bindings.md.uLy2BLbT.js b/assets/stacks_ko_client_bindings.md.uLy2BLbT.js
new file mode 100644
index 000000000..b36af756a
--- /dev/null
+++ b/assets/stacks_ko_client_bindings.md.uLy2BLbT.js
@@ -0,0 +1,40 @@
+import{_ as a,D as n,o as p,c as i,I as o,R as l,k as e,a as t}from"./chunks/framework.g9eZ-ZSs.js";const me=JSON.parse('{"title":"Knockout Bindings","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/ko/client/bindings.md","filePath":"stacks/ko/client/bindings.md"}'),c={name:"stacks/ko/client/bindings.md"},r=l(`
Coalesce provides a number of knockout bindings that make common model binding activities much easier.
Editors Note: On this page, some bindings are split into their requisite HTML component with their data-bind component listed immediately after. Keep this in mind when reading.
Creates a select2 dropdown using the specified url and fields that can be used to select an object from the endpoint specified. Additional complimentary bindings include:
`,7),d=e("p",null,"The Coalesce List API url to call to populate the contents of the dropdown.",-1),h=e("p",null,[t("The name of the field on each item in the results of the AJAX call which contains the ID of the option. The value of this field will be set on the observable specified for the main "),e("code",null,"select2Ajax"),t(" binding.")],-1),u=e("p",null,"The name of the field on each item in the results of the AJAX call which contains the text to be displayed for each option.",-1),y=e("p",null,[t("An observable that holds the full object corresponding to the foreign key property being bound to. If the selected value changes, this will be set to null to avoid representation of incorrect data (unless "),e("code",null,"setObject"),t(" is used - see below).")],-1),m=l("
If true, the observable specified by the object binding will be set to the selected data when an option is chosen in the dropdown. Binding itemViewModel is required if this binding is set.
Additionally, requests to the API to populate the dropdown will request the entire object, as opposed to only the two fields specified for idField and textField like is normally done when this binding is missing or set to false. To override this behavior and continue requesting only specific fields even when setObject is true, add fields=field1,field2,... to the query string of the url binding.
",2),b=e("p",null,[t("A reference to the class that represents the type of the object held in the "),e("code",null,"object"),t(" observable. This is used when constructing new objects from the results of the API call. Not used if "),e("code",null,"setObject"),t(" is false or unspecified. For example, "),e("code",null,"setObject: true, itemViewModel: ViewModels.Person"),t(".")],-1),f=e("p",null,"The number of items to request in each call to the server.",-1),g=e("p",null,[t("A string containing the substring "),e("code",null,"{0}"),t(", which will be replaced with the text value of an option in the dropdown list when the option is displayed.")],-1),D=e("p",null,[t("A string containing the substring "),e("code",null,"{0}"),t(", which will be replaced with the text value of the selected option of the dropdown list.")],-1),C=e("p",null,"If true, a cache-busting querystring parameter will be included in AJAX requests.",-1),_=e("p",null,[t("Directly maps to select2 option "),e("code",null,"selectOnClose"),t(".")],-1),w=e("p",null,[t("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),t(".")],-1),v=e("p",null,[t("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),t(".")],-1),k=l(`
If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.
Creates a select2 multi-select input for choosing objects that participate as the foreign object in a many-to-many relationship with the current object. The primary select2AjaxMultiple binding takes the collection of items that make up the foreign side of the relationship. This is NOT the collection of the join objects (a.k.a. middle table objects) in the relationship.
Additional complimentary bindings include:
`,5),x=e("p",null,[t("The Coalesce List API url to call to populate the contents of the dropdown. In order to only receive specific fields from the server, add "),e("code",null,"fields=field1,field2,..."),t(" to the query string of the url, ensuring that at least the "),e("code",null,"idField"),t(" and "),e("code",null,"textField"),t(" are included in that collection.")],-1),q=e("p",null,"The name of the field on each item in the results of the AJAX call which contains the ID of the option. The value of this field will be set as the key of the foreign object in the many-to-many relationship.",-1),T=e("p",null,"The name of the field on each item in the results of the AJAX call which contains the text to be displayed for each option.",-1),A=e("p",null,[t("A reference to the class that represents the types in the supplied collection. For example, a many-to-many between "),e("code",null,"Person"),t(" and "),e("code",null,"Case"),t(" objects where "),e("code",null,"Case"),t(" is the object being bound to and "),e("code",null,"Person"),t(" is the type represented by a child collection, the correct value is "),e("code",null,"ViewModels.Person"),t(". This is used when constructing new objects representing the relationship when a new item is selected.")],-1),j=e("p",null,"The number of items to request in each call to the server.",-1),E=e("p",null,[t("A string containing the substring "),e("code",null,"{0}"),t(", which will be replaced with the text value of an option in the dropdown list when the option is displayed.")],-1),F=e("p",null,[t("A string containing the substring "),e("code",null,"{0}"),t(", which will be replaced with the text value of the selected option of the dropdown list.")],-1),I=e("p",null,"If true, a cache-busting querystring parameter will be included in AJAX requests.",-1),P=e("p",null,[t("Directly maps to select2 option "),e("code",null,"selectOnClose"),t(".")],-1),S=e("p",null,[t("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),t(".")],-1),M=e("p",null,[t("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),t(".")],-1),V=l(`
If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.
Creates a select2 dropdown against the specified url where the url returns a collection of string values that are potential selection candidates. The dropdown also allows the user to input any value they choose - the API simply serves suggested values.
`,4),O=l("
The url to call to populate the contents of the dropdown. This should be an endpoint that returns one of the following:
A raw string[]
An object that conforms to { list: string[] }
An object that conforms to { object: string[] }
An object that conforms to { list: { [prop: string]: string } } where the value given to resultField is a valid property of the returned objects.
An object that conforms to { object: { [prop: string]: string } } where the value given to resultField is a valid property of the returned objects.
The url will also be passed a search parameter and a page parameter appended to the query string. The chosen endpoint is responsible for implementing this functionality. Page size is expected to be some fixed value. Implementer should anticipate that the requested page may be out of range.
The cases listed above that accept arrays of objects (as opposed to arrays of strings) require that the resultField binding is also used. These are designed for obtaining string values from objects obtained from the standard list endpoint.
",4),B=e("p",null,[t("If provided, specifies a field on the objects returned from the API to pull the string values from. See examples in "),e("code",null,"url"),t(" above.")],-1),N=e("p",null,[t("If "),e("code",null,"false"),t(", the user's search input will not be presented as a valid selectable value; only the exact values obtained from the API endpoint will be selectable.")],-1),K=e("p",null,[t("Directly maps to select2 option "),e("code",null,"selectOnClose"),t(".")],-1),Y=e("p",null,[t("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),t(".")],-1),z=e("p",null,[t("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),t(".")],-1),R=l(`
If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.
Sets up a basic select2 dropdown on an HTML select element. Dropdown contents should be populated through other means - either using stock Knockout bindings or server-side static contents (via cshtml).
`,4),$=e("p",null,[t("Directly maps to select2 option "),e("code",null,"selectOnClose"),t(".")],-1),W=e("p",null,[t("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),t(".")],-1),J=e("p",null,[t("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),t(".")],-1),L=l(`
If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.
Creates a date/time picker for changing a moment.Moment property. The control used is bootstrap-datetimepicker
`,4),X=e("p",null,[t("If true, the date portion of the "),e("code",null,"moment.Moment"),t(" object will be preserved by the date picker. Only the time portion will be changed by user input.")],-1),H=e("p",null,[t("If true, the time portion of the "),e("code",null,"moment.Moment"),t(" object will be preserved by the date picker. Only the date portion will be changed by user input.")],-1),Q=e("p",null,[t("Specify the moment-compatible format string to be used as the display format for the text value shown on the date picker. Defaults to "),e("code",null,"M/D/YY h:mm a"),t(". Direct pass-through to "),e("a",{href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noreferrer"},"bootstrap-datetimepicker"),t(".")],-1),U=e("p",null,[t("If true, places the time picker next to the date picker, visible at the same time. Direct pass-through to corresponding "),e("a",{href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noreferrer"},"bootstrap-datetimepicker"),t(" option.")],-1),Z=e("p",null,[t("Direct pass-through to corresponding "),e("a",{href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noreferrer"},"bootstrap-datetimepicker"),t(" option.")],-1),G=e("p",null,[t("Direct pass-through to corresponding "),e("a",{href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noreferrer"},"bootstrap-datetimepicker"),t(" option.")],-1),ee=e("p",null,[t("Override key bindings of the date picker. Direct pass-through to corresponding "),e("a",{href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noreferrer"},"bootstrap-datetimepicker"),t(" option.")],-1),te=l(`
If true, the datePicker will update the underlying observable on each input change. Otherwise, the observable will only be changed when the datePicker loses focus (on blur).
When used in a context where $data is a Coalesce.BaseViewModel, that object's saveTimeoutMs configuration property (see ViewModel Configuration) will be set to 0 when the element it is placed on gains focus. This value will be reverted to its previous value when the element loses focus. This will cause any changes to the object, including any observable bound as input on the element, to trigger a save immediately rather than after a delay (defaults to 500ms).
When used in a context where $data is a Coalesce.BaseViewModel, that object's autoSaveEnabled configuration property (see ViewModel Configuration) will be set to false when the element it is placed on gains focus. This will cause any changes to the object, including any observable bound as input on the element, to not trigger auto saves while the element has focus. When the element loses focus, the autoSaveEnabled flag will be reverted to its previous value and an attempt will be made to save the object.
Wrapper around the Bootstrap tooltip component. Binding can either be simply a string (or observable string), or it can be an object that will be passed directly to the Bootstrap tooltip component.
The let binding is a somewhat common construct used in Knockout applications, but isn't part of Knockout itself. It effectively allows the creation of variables in the binding context, allowing complex statements which may be used multiple times to be aliased for both clarity of code and better performance.
These are static properties on IntelliTect.Coalesce.Knockout.Helpers.Knockout you can assign to somewhere in the app lifecycle startup to change the default markup generated server-side when using @Knockout.* methods to render Knockout bindings in your .cshtml files.
`,30),se=e("p",null,"The default number of Bootstrap grid columns a field label should span across.",-1),oe=e("p",null,"The default number of Bootstrap grid columns a form input should span across.",-1),le=e("p",null,[t("Sets the default date-only format to be used by all date/time pickers. This only applies to models with a date-only "),e("a",{href:"/Coalesce/modeling/model-components/attributes/date-type.html"},"[DateType]"),t(" attribute.")],-1),ae=e("p",null,[t("Sets the default time-only format to be used by all date/time pickers. This only applies to models with a time-only "),e("a",{href:"/Coalesce/modeling/model-components/attributes/date-type.html"},"[DateType]"),t(" attribute.")],-1),ne=l(`
Sets the default date/time format to be used by all date/time pickers. This only applies to DateTimeOffset model properties that do not have a limiting [DateType] attribute.
Note
DefaultDateFormat, DefaultTimeFormat and DefaultDateTimeFormat all take various formatting strings from the Moment.js library. A full listing can be found on the Moment website.
The date/time picker properties can be coupled with DateTimeOffset model properties to display time values localized for the current user's locale. If you want to make the localization static, simply include a script block in your _Layout.cshtml or in a specific view that sets the default for Moment.js:
This needs to happen after Moment is loaded, but before the bootstrap-datetimepicker script is loaded.
`,6);function pe(ie,ce,re,de,he,ue){const s=n("Prop");return p(),i("div",null,[r,o(s,{def:"url: string",lang:"ts","id-prefix":"select2Ajax"}),d,o(s,{def:"idField: string",lang:"ts","id-prefix":"select2Ajax"}),h,o(s,{def:"textField: string",lang:"ts","id-prefix":"select2Ajax"}),u,o(s,{def:"object?: KnockoutObservable",lang:"ts","id-prefix":"select2Ajax"}),y,o(s,{def:"setObject: boolean = false",lang:"ts","id-prefix":"select2Ajax"}),m,o(s,{def:"itemViewModel?: (new (newItem: object) => Coalesce.BaseViewModel)",lang:"ts","id-prefix":"select2Ajax"}),b,o(s,{def:"pageSize: number = 25",lang:"ts","id-prefix":"select2Ajax"}),f,o(s,{def:"format: string = '{0}'",lang:"ts","id-prefix":"select2Ajax"}),g,o(s,{def:"selectionFormat: string = '{0}'",lang:"ts","id-prefix":"select2Ajax"}),D,o(s,{def:"cache: boolean = true",lang:"ts","id-prefix":"select2Ajax"}),C,o(s,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2Ajax"}),_,o(s,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2Ajax"}),w,o(s,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2Ajax"}),v,o(s,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2Ajax"}),k,o(s,{def:"url: string",lang:"ts","id-prefix":"select2AjaxMultiple"}),x,o(s,{def:"idField: string",lang:"ts","id-prefix":"select2AjaxMultiple"}),q,o(s,{def:"textField: string",lang:"ts","id-prefix":"select2AjaxMultiple"}),T,o(s,{def:"itemViewModel: (new (newItem: object) => Coalesce.BaseViewModel)",lang:"ts","id-prefix":"select2AjaxMultiple"}),A,o(s,{def:"pageSize: number = 25",lang:"ts","id-prefix":"select2AjaxMultiple"}),j,o(s,{def:"format: string = '{0}'",lang:"ts","id-prefix":"select2AjaxMultiple"}),E,o(s,{def:"selectionFormat: string = '{0}'",lang:"ts","id-prefix":"select2AjaxMultiple"}),F,o(s,{def:"cache: boolean = true",lang:"ts","id-prefix":"select2AjaxMultiple"}),I,o(s,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2AjaxMultiple"}),P,o(s,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2AjaxMultiple"}),S,o(s,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2AjaxMultiple"}),M,o(s,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2AjaxMultiple"}),V,o(s,{def:"url: string",lang:"ts","id-prefix":"select2AjaxText"}),O,o(s,{def:"resultField?: string",lang:"ts","id-prefix":"select2AjaxText"}),B,o(s,{def:"allowCustom: boolean = true",lang:"ts","id-prefix":"select2AjaxText"}),N,o(s,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2AjaxText"}),K,o(s,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2AjaxText"}),Y,o(s,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2AjaxText"}),z,o(s,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2AjaxText"}),R,o(s,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2"}),$,o(s,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2"}),W,o(s,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2"}),J,o(s,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2"}),L,o(s,{def:"preserveDate: boolean = false",lang:"ts","id-prefix":"date-picker"}),X,o(s,{def:"preserveTime: boolean = false",lang:"ts","id-prefix":"date-picker"}),H,o(s,{def:"format: string = 'M/D/YY h:mm a'",lang:"ts","id-prefix":"date-picker"}),Q,o(s,{def:"sideBySide: boolean = false",lang:"ts","id-prefix":"date-picker"}),U,o(s,{def:"stepping: number = 1",lang:"ts","id-prefix":"date-picker"}),Z,o(s,{def:"timeZone: string = ''",lang:"ts","id-prefix":"date-picker"}),G,o(s,{def:"keyBinds = { left: null, right: null, delete: null }",lang:"ts","id-prefix":"date-picker"}),ee,o(s,{def:"updateImmediate: boolean = false",lang:"ts","id-prefix":"date-picker"}),te,o(s,{def:"public static int DefaultLabelCols { get; set; } = 3;"}),se,o(s,{def:"public static int DefaultInputCols { get; set; } = 9;"}),oe,o(s,{def:'public static string DefaultDateFormat { get; set; } = "M/D/YYYY";'}),le,o(s,{def:'public static string DefaultTimeFormat { get; set; } = "h:mm a";'}),ae,o(s,{def:'public static string DefaultDateTimeFormat { get; set; } = "M/D/YYYY h:mm a";'}),ne])}const be=a(c,[["render",pe]]);export{me as __pageData,be as default};
diff --git a/assets/stacks_ko_client_bindings.md.uLy2BLbT.lean.js b/assets/stacks_ko_client_bindings.md.uLy2BLbT.lean.js
new file mode 100644
index 000000000..ec48c21de
--- /dev/null
+++ b/assets/stacks_ko_client_bindings.md.uLy2BLbT.lean.js
@@ -0,0 +1 @@
+import{_ as a,D as n,o as p,c as i,I as o,R as l,k as e,a as t}from"./chunks/framework.g9eZ-ZSs.js";const me=JSON.parse('{"title":"Knockout Bindings","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/ko/client/bindings.md","filePath":"stacks/ko/client/bindings.md"}'),c={name:"stacks/ko/client/bindings.md"},r=l("",7),d=e("p",null,"The Coalesce List API url to call to populate the contents of the dropdown.",-1),h=e("p",null,[t("The name of the field on each item in the results of the AJAX call which contains the ID of the option. The value of this field will be set on the observable specified for the main "),e("code",null,"select2Ajax"),t(" binding.")],-1),u=e("p",null,"The name of the field on each item in the results of the AJAX call which contains the text to be displayed for each option.",-1),y=e("p",null,[t("An observable that holds the full object corresponding to the foreign key property being bound to. If the selected value changes, this will be set to null to avoid representation of incorrect data (unless "),e("code",null,"setObject"),t(" is used - see below).")],-1),m=l("",2),b=e("p",null,[t("A reference to the class that represents the type of the object held in the "),e("code",null,"object"),t(" observable. This is used when constructing new objects from the results of the API call. Not used if "),e("code",null,"setObject"),t(" is false or unspecified. For example, "),e("code",null,"setObject: true, itemViewModel: ViewModels.Person"),t(".")],-1),f=e("p",null,"The number of items to request in each call to the server.",-1),g=e("p",null,[t("A string containing the substring "),e("code",null,"{0}"),t(", which will be replaced with the text value of an option in the dropdown list when the option is displayed.")],-1),D=e("p",null,[t("A string containing the substring "),e("code",null,"{0}"),t(", which will be replaced with the text value of the selected option of the dropdown list.")],-1),C=e("p",null,"If true, a cache-busting querystring parameter will be included in AJAX requests.",-1),_=e("p",null,[t("Directly maps to select2 option "),e("code",null,"selectOnClose"),t(".")],-1),w=e("p",null,[t("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),t(".")],-1),v=e("p",null,[t("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),t(".")],-1),k=l("",5),x=e("p",null,[t("The Coalesce List API url to call to populate the contents of the dropdown. In order to only receive specific fields from the server, add "),e("code",null,"fields=field1,field2,..."),t(" to the query string of the url, ensuring that at least the "),e("code",null,"idField"),t(" and "),e("code",null,"textField"),t(" are included in that collection.")],-1),q=e("p",null,"The name of the field on each item in the results of the AJAX call which contains the ID of the option. The value of this field will be set as the key of the foreign object in the many-to-many relationship.",-1),T=e("p",null,"The name of the field on each item in the results of the AJAX call which contains the text to be displayed for each option.",-1),A=e("p",null,[t("A reference to the class that represents the types in the supplied collection. For example, a many-to-many between "),e("code",null,"Person"),t(" and "),e("code",null,"Case"),t(" objects where "),e("code",null,"Case"),t(" is the object being bound to and "),e("code",null,"Person"),t(" is the type represented by a child collection, the correct value is "),e("code",null,"ViewModels.Person"),t(". This is used when constructing new objects representing the relationship when a new item is selected.")],-1),j=e("p",null,"The number of items to request in each call to the server.",-1),E=e("p",null,[t("A string containing the substring "),e("code",null,"{0}"),t(", which will be replaced with the text value of an option in the dropdown list when the option is displayed.")],-1),F=e("p",null,[t("A string containing the substring "),e("code",null,"{0}"),t(", which will be replaced with the text value of the selected option of the dropdown list.")],-1),I=e("p",null,"If true, a cache-busting querystring parameter will be included in AJAX requests.",-1),P=e("p",null,[t("Directly maps to select2 option "),e("code",null,"selectOnClose"),t(".")],-1),S=e("p",null,[t("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),t(".")],-1),M=e("p",null,[t("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),t(".")],-1),V=l("",4),O=l("",4),B=e("p",null,[t("If provided, specifies a field on the objects returned from the API to pull the string values from. See examples in "),e("code",null,"url"),t(" above.")],-1),N=e("p",null,[t("If "),e("code",null,"false"),t(", the user's search input will not be presented as a valid selectable value; only the exact values obtained from the API endpoint will be selectable.")],-1),K=e("p",null,[t("Directly maps to select2 option "),e("code",null,"selectOnClose"),t(".")],-1),Y=e("p",null,[t("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),t(".")],-1),z=e("p",null,[t("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),t(".")],-1),R=l("",4),$=e("p",null,[t("Directly maps to select2 option "),e("code",null,"selectOnClose"),t(".")],-1),W=e("p",null,[t("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),t(".")],-1),J=e("p",null,[t("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),t(".")],-1),L=l("",4),X=e("p",null,[t("If true, the date portion of the "),e("code",null,"moment.Moment"),t(" object will be preserved by the date picker. Only the time portion will be changed by user input.")],-1),H=e("p",null,[t("If true, the time portion of the "),e("code",null,"moment.Moment"),t(" object will be preserved by the date picker. Only the date portion will be changed by user input.")],-1),Q=e("p",null,[t("Specify the moment-compatible format string to be used as the display format for the text value shown on the date picker. Defaults to "),e("code",null,"M/D/YY h:mm a"),t(". Direct pass-through to "),e("a",{href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noreferrer"},"bootstrap-datetimepicker"),t(".")],-1),U=e("p",null,[t("If true, places the time picker next to the date picker, visible at the same time. Direct pass-through to corresponding "),e("a",{href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noreferrer"},"bootstrap-datetimepicker"),t(" option.")],-1),Z=e("p",null,[t("Direct pass-through to corresponding "),e("a",{href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noreferrer"},"bootstrap-datetimepicker"),t(" option.")],-1),G=e("p",null,[t("Direct pass-through to corresponding "),e("a",{href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noreferrer"},"bootstrap-datetimepicker"),t(" option.")],-1),ee=e("p",null,[t("Override key bindings of the date picker. Direct pass-through to corresponding "),e("a",{href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noreferrer"},"bootstrap-datetimepicker"),t(" option.")],-1),te=l("",30),se=e("p",null,"The default number of Bootstrap grid columns a field label should span across.",-1),oe=e("p",null,"The default number of Bootstrap grid columns a form input should span across.",-1),le=e("p",null,[t("Sets the default date-only format to be used by all date/time pickers. This only applies to models with a date-only "),e("a",{href:"/Coalesce/modeling/model-components/attributes/date-type.html"},"[DateType]"),t(" attribute.")],-1),ae=e("p",null,[t("Sets the default time-only format to be used by all date/time pickers. This only applies to models with a time-only "),e("a",{href:"/Coalesce/modeling/model-components/attributes/date-type.html"},"[DateType]"),t(" attribute.")],-1),ne=l("",6);function pe(ie,ce,re,de,he,ue){const s=n("Prop");return p(),i("div",null,[r,o(s,{def:"url: string",lang:"ts","id-prefix":"select2Ajax"}),d,o(s,{def:"idField: string",lang:"ts","id-prefix":"select2Ajax"}),h,o(s,{def:"textField: string",lang:"ts","id-prefix":"select2Ajax"}),u,o(s,{def:"object?: KnockoutObservable",lang:"ts","id-prefix":"select2Ajax"}),y,o(s,{def:"setObject: boolean = false",lang:"ts","id-prefix":"select2Ajax"}),m,o(s,{def:"itemViewModel?: (new (newItem: object) => Coalesce.BaseViewModel)",lang:"ts","id-prefix":"select2Ajax"}),b,o(s,{def:"pageSize: number = 25",lang:"ts","id-prefix":"select2Ajax"}),f,o(s,{def:"format: string = '{0}'",lang:"ts","id-prefix":"select2Ajax"}),g,o(s,{def:"selectionFormat: string = '{0}'",lang:"ts","id-prefix":"select2Ajax"}),D,o(s,{def:"cache: boolean = true",lang:"ts","id-prefix":"select2Ajax"}),C,o(s,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2Ajax"}),_,o(s,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2Ajax"}),w,o(s,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2Ajax"}),v,o(s,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2Ajax"}),k,o(s,{def:"url: string",lang:"ts","id-prefix":"select2AjaxMultiple"}),x,o(s,{def:"idField: string",lang:"ts","id-prefix":"select2AjaxMultiple"}),q,o(s,{def:"textField: string",lang:"ts","id-prefix":"select2AjaxMultiple"}),T,o(s,{def:"itemViewModel: (new (newItem: object) => Coalesce.BaseViewModel)",lang:"ts","id-prefix":"select2AjaxMultiple"}),A,o(s,{def:"pageSize: number = 25",lang:"ts","id-prefix":"select2AjaxMultiple"}),j,o(s,{def:"format: string = '{0}'",lang:"ts","id-prefix":"select2AjaxMultiple"}),E,o(s,{def:"selectionFormat: string = '{0}'",lang:"ts","id-prefix":"select2AjaxMultiple"}),F,o(s,{def:"cache: boolean = true",lang:"ts","id-prefix":"select2AjaxMultiple"}),I,o(s,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2AjaxMultiple"}),P,o(s,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2AjaxMultiple"}),S,o(s,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2AjaxMultiple"}),M,o(s,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2AjaxMultiple"}),V,o(s,{def:"url: string",lang:"ts","id-prefix":"select2AjaxText"}),O,o(s,{def:"resultField?: string",lang:"ts","id-prefix":"select2AjaxText"}),B,o(s,{def:"allowCustom: boolean = true",lang:"ts","id-prefix":"select2AjaxText"}),N,o(s,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2AjaxText"}),K,o(s,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2AjaxText"}),Y,o(s,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2AjaxText"}),z,o(s,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2AjaxText"}),R,o(s,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2"}),$,o(s,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2"}),W,o(s,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2"}),J,o(s,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2"}),L,o(s,{def:"preserveDate: boolean = false",lang:"ts","id-prefix":"date-picker"}),X,o(s,{def:"preserveTime: boolean = false",lang:"ts","id-prefix":"date-picker"}),H,o(s,{def:"format: string = 'M/D/YY h:mm a'",lang:"ts","id-prefix":"date-picker"}),Q,o(s,{def:"sideBySide: boolean = false",lang:"ts","id-prefix":"date-picker"}),U,o(s,{def:"stepping: number = 1",lang:"ts","id-prefix":"date-picker"}),Z,o(s,{def:"timeZone: string = ''",lang:"ts","id-prefix":"date-picker"}),G,o(s,{def:"keyBinds = { left: null, right: null, delete: null }",lang:"ts","id-prefix":"date-picker"}),ee,o(s,{def:"updateImmediate: boolean = false",lang:"ts","id-prefix":"date-picker"}),te,o(s,{def:"public static int DefaultLabelCols { get; set; } = 3;"}),se,o(s,{def:"public static int DefaultInputCols { get; set; } = 9;"}),oe,o(s,{def:'public static string DefaultDateFormat { get; set; } = "M/D/YYYY";'}),le,o(s,{def:'public static string DefaultTimeFormat { get; set; } = "h:mm a";'}),ae,o(s,{def:'public static string DefaultDateTimeFormat { get; set; } = "M/D/YYYY h:mm a";'}),ne])}const be=a(c,[["render",pe]]);export{me as __pageData,be as default};
diff --git a/assets/stacks_ko_client_external-view-model.md.mJX84X3y.js b/assets/stacks_ko_client_external-view-model.md.mJX84X3y.js
new file mode 100644
index 000000000..b86123977
--- /dev/null
+++ b/assets/stacks_ko_client_external-view-model.md.mJX84X3y.js
@@ -0,0 +1,8 @@
+import{_ as a,D as l,o as n,c as s,I as r,R as i,k as e,a as o}from"./chunks/framework.g9eZ-ZSs.js";const T=JSON.parse('{"title":"TypeScript External ViewModels","description":"","frontmatter":{},"headers":[],"relativePath":"stacks/ko/client/external-view-model.md","filePath":"stacks/ko/client/external-view-model.md"}'),c={name:"stacks/ko/client/external-view-model.md"},p=i('
For all External Types in your model, Coalesce will generate a TypeScript class that provides a bare-bones representation of that type's properties.
These ViewModels are dependent on Knockout, and are designed to be used directly from Knockout bindings in your HTML. All data properties on the generated model are Knockout observables.
The TypeScript ViewModels for external types do not have a common base class, and do not have any of the behaviors or convenience properties that the regular TypeScript ViewModels for database-mapped classes have.
',7),d=e("p",null,[o("For each exposed property on the underlying EF POCO, a "),e("code",null,"KnockoutObservable"),o(" property will exist on the TypeScript model. For navigation properties, these will be typed with the corresponding TypeScript ViewModel for the other end of the relationship. For collections (including collection navigation properties), these properties will be "),e("code",null,"KnockoutObservableArray"),o(" objects.")],-1),b=e("h3",{id:"enum-members",tabindex:"-1"},[o("Enum Members "),e("a",{class:"header-anchor",href:"#enum-members","aria-label":'Permalink to "Enum Members"'},"")],-1),m=e("p",null,[o("For each "),e("code",null,"enum"),o(" property on your POCO, the following will be created:")],-1),u=e("p",null,[o("A "),e("code",null,"KnockoutComputed"),o(" property that will provide the text to display for that property.")],-1);function h(k,_,v,y,f,x){const t=l("Prop");return n(),s("div",null,[p,r(t,{def:`
+public personId: KnockoutObservable = ko.observable(null);
+public fullName: KnockoutObservable = ko.observable(null);
+public gender: KnockoutObservable = ko.observable(null);
+public companyId: KnockoutObservable