+
+
+
+
\ 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.