ServiceStack - [Reference] or [Ignore]? - servicestack
We have a DTO - Employee - with many (> 20) related DTOs and DTO collections. For "size of returned JSON" reasons, we have marked those relationships as [Ignore]. It is then up to the client to populate any related DTOs that they would like using other REST calls.
We have tried a couple of things to satisfy clients' desire to have some related Employee info but not all:
We created a new DTO - EmployeeLite - which has the most-requested fields defined with "RelatedTableNameRelatedFieldName" approach and used the QueryBase overload and that has worked well.
We've also tried adding a property to a request DTO - "References" - which is a comma-separated list of related DTOs that the client would like populated. We then iterate the response and populate each Employee with the related DTO or List. The concern there is performance when iterating a large List.
We're wondering if there a suggested approach to what we're trying to do?
Thanks for any suggestions you may have.
UPDATE:
Here is a portion of our request DTO:
[Route("/employees", "GET")]
public class FindEmployeesRequest : QueryDb<Employee> {
public int? ID { get; set; }
public int[] IDs { get; set; }
public string UserID { get; set; }
public string LastNameStartsWith { get; set; }
public DateTime[] DateOfBirthBetween { get; set; }
public DateTime[] HireDateBetween { get; set; }
public bool? IsActive { get; set; }
}
There is no code for the service (automagical with QueryDb), so I added some to try the "merge" approach:
public object Get(FindEmployeesRequest request) {
var query = AutoQuery.CreateQuery(request, Request.GetRequestParams());
QueryResponse<Employee> response = AutoQuery.Execute(request, query);
if (response.Total > 0) {
List<Clerkship> clerkships = Db.Select<Clerkship>();
response.Results.Merge(clerkships);
}
return response;
}
This fails with Could not find Child Reference for 'Clerkship' on Parent 'Employee'
because in Employee we have:
[Ignore]
public List<Clerkship> Clerkships { get; set; }
which we did because we don't want "Clerkships" with every request. If I change [Ignore] to [Reference] I don't need the code above in the service - the List comes automatically. So it seems that .Merge only works with [Reference] which we don't want to do.
I'm not sure how I would use the "Custom Load References" approach in an AutoQuery service. And, AFAIKT, the "Custom Fields" approach can't be use for related DTOs, only for fields in the base table.
UPDATE 2:
The LoadSelect with include[] is working well for us. We are now trying to cover the case where ?fields= is used in the query string but the client does not request the ID field of the related DTO:
public partial class Employee {
[PrimaryKey]
[AutoIncrement]
public int ID { get; set; }
.
.
.
[References(typeof(Department))]
public int DepartmentID { get; set; }
.
.
.
public class Department {
[PrimaryKey]
public int ID { get; set; }
public string Name { get; set; }
.
.
.
}
So, for the request
/employees?fields=id,departmentid
we will get the Department in the response. But for the request
/employees?fields=id
we won't get the Department in the response.
We're trying to "quietly fix" this for the requester by modifying the query.SelectExpression and adding , "Employee"."DepartmentID" to the SELECT before doing the Db.LoadSelect. Debugging shows that query.SelectExpression is being modified, but according to SQL Profiler, "Employee"."DepartmentID" is not being selected.
Is there something else we should be doing to get "Employee"."DepartmentID" added to the SELECT?
Thanks.
UPDATE 3:
The Employee table has three 1:1 relationships - EmployeeType, Department and Title:
public partial class Employee {
[PrimaryKey]
[AutoIncrement]
public int ID { get; set; }
[References(typeof(EmployeeType))]
public int EmployeeTypeID { get; set; }
[References(typeof(Department))]
public int DepartmentID { get; set; }
[References(typeof(Title))]
public int TitleID { get; set; }
.
.
.
}
public class EmployeeType {
[PrimaryKey]
public int ID { get; set; }
public string Name { get; set; }
}
public class Department {
[PrimaryKey]
public int ID { get; set; }
public string Name { get; set; }
[Reference]
public List<Title> Titles { get; set; }
}
public class Title {
[PrimaryKey]
public int ID { get; set; }
[References(typeof(Department))]
public int DepartmentID { get; set; }
public string Name { get; set; }
}
The latest update to 4.0.55 allows this:
/employees?fields=employeetype,department,title
I get back all the Employee table fields plus the three related DTOs - with one strange thing - the Employee's ID field is populated with the Employee's TitleID values (I think we saw this before?).
This request fixes that anomaly:
/employees?fields=id,employeetypeid,employeetype,departmentid,department,titleid,title
but I lose all of the other Employee fields.
This sounds like a "have your cake and eat it too" request, but is there a way that I can get all of the Employee fields and selective related DTOs? Something like:
/employees?fields=*,employeetype,department,title
AutoQuery Customizable Fields
Not sure if this is Relevant but AutoQuery has built-in support for Customizing which fields to return with the ?fields=Field1,Field2 option.
Merge disconnected POCO Results
As you've not provided any source code it's not clear what you're trying to achieve or where the inefficiency with the existing solution lies, but you don't want to be doing any N+1 SELECT queries. If you are, have a look at how you can merge disconnected POCO results together which will let you merge results from separate queries based on the relationships defined using OrmLite references, e.g the example below uses 2 distinct queries to join Customers with their orders:
//Select Customers who've had orders with Quantities of 10 or more
List<Customer> customers = db.Select<Customer>(q =>
q.Join<Order>()
.Where<Order>(o => o.Qty >= 10)
.SelectDistinct());
//Select Orders with Quantities of 10 or more
List<Order> orders = db.Select<Order>(o => o.Qty >= 10);
customers.Merge(orders); // Merge disconnected Orders with their related Customers
Custom Load References
You can selectively control which references OrmLite should load by specifying them when you call OrmLite's Load* API's, e.g:
var customerWithAddress = db.LoadSingleById<Customer>(customer.Id,
include: new[] { "PrimaryAddress" });
Using Custom Load References in AutoQuery
You can customize an AutoQuery Request to not return any references by using Db.Select instead of Db.LoadSelect in your custom AutoQuery implementation, e.g:
public object Get(FindEmployeesRequest request)
{
var q = AutoQuery.CreateQuery(request, Request);
var response = new QueryResponse<Employee>
{
Offset = q.Offset.GetValueOrDefault(0),
Results = Db.Select(q),
Total = (int)Db.Count(q),
};
return response;
}
Likewise if you only want to selectively load 1 or more references you can change LoadSelect to pass in an include: array with only the reference fields you want included, e.g:
public object Get(FindEmployeesRequest request)
{
var q = AutoQuery.CreateQuery(request, Request);
var response = new QueryResponse<Employee>
{
Offset = q.Offset.GetValueOrDefault(0),
Results = Db.LoadSelect(q, include:new []{ "Clerkships" }),
Total = (int)Db.Count(q),
};
return response;
}
Related
Servicestack - possibility of mapping several POCO to one table
I'm looking for a way to map several POCO objects into single table in the ServiceStack. Is it possible to do this in a clean way, without "hacking" table creation process?
As a general rule, In OrmLite: 1 Class = 1 Table. But I'm not clear what you mean my "map several POCO objects into single table", it sounds like using Auto Mapping to populate a table with multiple POCO instances, e.g: var row = db.SingleById<Table>(id); row.PopulateWithNonDefaultValues(instance1); row.PopulateWithNonDefaultValues(instance2); db.Update(row); If you need to maintain a single table and have other "sub" classes that maintain different table in the universal table you can use [Alias] so all Update/Select/Insert's reference the same table, e.g: public class Poco { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } } [Alias(nameof(Poco))] public class PocoName { public int Id { get; set; } public string Name { get; set; } } [Alias(nameof(Poco))] public class PocoAge { public int Id { get; set; } public int Age { get; set; } } Although I don't really see the benefit over having a single table that you use AutoMapping to map your other classes to before using that in OrmLite.
EF5 Object property won't save in a database
So I'm creating a simple MVC app that uses the Absence class as a model which holds different properties including an object from the Employee class-another model that holds various properties: public class Absence { public int Id { get; set; } public string Reason { get; set; } public int Day { get; set; } public int Month { get; set; } public bool isApproved { get; set; } public int employee_Id { get; set; } public virtual Employee employee { get; set; } public Absence() { employee = new Employee(); } } And I created a controller that has an ActionResult function for the create View: [HttpGet] public ActionResult Create() { Absence abs = new Absence(); return View(abs); } [HttpPost] public ActionResult Create(Absence abb) { Employee emp = database.Employees.FirstOrDefault(z => z.Id == abb.employee_Id); System.Diagnostics.Debug.WriteLine(emp.Name); abb.employee.Name = emp.Name; abb.employee.Surname = emp.Surname; System.Diagnostics.Debug.WriteLine(abb.employee.Name); database.Absences.Add(abb); database.SaveChanges(); return Redirect("/Absence"); } The idea is to talk to the database find an Employee object with the same EmployeeId and set the name and surname of the employee object of the abb object to be the same and after testing it with the debugger I can see that it works. However when I want to display all the added absences including the name and surname of their employee like this: public ActionResult Index() { return View(database.Absences.ToList()); } The name and surname of all the employees don't show. It seems that all the properties are saved in the database using entity framework except for the Employee object. Any ideas for how to save it?
Can you try saving the Absences first before updating the employee details? I don't exactly know how your database models are configured and this is just an assumption. The absence that is referencing your employee is not yet available when you are supplying the employee details. Employee emp = database.Employees.FirstOrDefault(z => z.Id == abb.employee_Id); database.Absences.Add(abb); //Moved adding here database.SaveChanges(); // Perform the saving abb.employee.Name = emp.Name; abb.employee.Surname = emp.Surname; database.SaveChanges();
ServiceStack AutoQuery not working for DateTime values
I have a ServiceStack service using autoquery where the DateTime greater than or less than are being ignored. Here is my request DTO: public class GetSources : QueryBase<DbSource, Source> { public string Name { get; set; } public string NameContains { get; set; } public string NameStartsWith { get; set; } public DateTime? LastUpdatedDateGreaterThan { get; set; } public DateTime? LastUpdatedDateLessThan { get; set; } } The database table poco generated from the ormlite T4 template looks like this: [Alias("DbSources")] [Schema("SomeSchema")] public partial class DbSource { [AutoIncrement] public int Id { get; set;} [Required] public string Name { get; set;} [Required] public DateTime LastUpdatedDate { get; set;} } In the service I do some validation and then use AutoQuery like this: var q = AutoQuery.CreateQuery(dto, Request.GetRequestParams()); q.Join<DbSource, CompanySource>((source, companySource) => source.Id == companySource.SourceId && companySource.CompanyID == companyId); return AutoQuery.Execute(dto, q); I'm using mstest [TestMethod] public void GetSources_LastUpdatedGreaterThan() { var expected = DateTime.Now; var query = new GetSources { LastUpdatedDateGreaterThan = expected}; QueryResponse<Source> result; using (var service = appHost.Container.Resolve<SourceService>()) { service.Request = new MockHttpRequest(); result = service.Any(query); } log.Info(result.ToJson()); result.Results.ForEach(src => Assert.IsTrue(src.LastUpdatedDate > expected)); } Name, NameContains, and NameStartsWith all work as expected in other tests, but both LastUpdatedDateGreaterThan and LastUpdatedDateLessThan do not generate a where clause. In my AutoQuery setup all of the properties are defaults except for EnableUntypedQueries which is false. I know I can explicitly add the where for them in the service. i.e. q.Where(source => source.LastUpdatedDate > dto.LastUpdatedDateGreaterThan); But if possible I would like AutoQuery to take care of it. Does DateTime work with AutoQuery? Or am I doing something wrong in my code.
I ran through the Servicestack AutoQuery unit tests that mythz created using sql server. I tested using a database view which my original project was querying against and I wasn't able to replicate the issue I'm having. For now I'm going to just add the QueryField attribute to the DateTime properties on the Query model like this: [QueryField(Template = "{Field} > {Value}", Field = "LastUpdatedDate")] public DateTime? LastUpdatedDateGreaterThan { get; set; } Adding the attribute gives me what I need. I'll spend some time tracking down the culprit in my code later.
Breeze doesn't expand TPH entities correctly
Breeze doesn't expand TPH entities correctly. When using expand in breeze if you are using TPH expand will only work for the first entity, the others properties will be null. If I change the entity not to use inheritances it works fine. I've also tested returning each entity separately in an expand query that also worked fine. //client side code var getResidentById = function (id, obs) { var query = EntityQuery.from('Residents') .where('id', '==', id) .expand('user, currentUnit, leases, leases.unit, leases.leaseStatus'); return manager.executeQuery(query).then(function (data) { if (obs) { obs(data.results[0]) } }, queryFailed); }; //Controler Endpoint [HttpGet] public IQueryable<Resident> { return _context.Context.UserDetails.OfType<Resident>(); } //Model public class UserDetail : EntityBase<int>, IArchivable, IHasPhoto, IDeactivatableEntity, IUpdatable { public bool IsArchived { get; set; } public int LastUpdatedById { get; set; } public UserProfile LastUpdatedBy { get; set; } public DateTimeOffset LastUpdatedDate { get; set; } public string PhotoUri { get; set; } public bool IsInactive { get; set; } } public abstract class UserBelongsToApartmentComplex : UserDetail, IBelongsToApartmentComplex { public int ApartmentComplexId { get; set; } public virtual ApartmentComplex ApartmentComplex { get; set; } public virtual bool IsInSameComplexAs(IRelatedToApartmentComplex otherEntity) { return ApartmentComplexId == otherEntity.ApartmentComplexId; } } public class Staff : UserBelongsToApartmentComplex { public string Title { get; set; } } public class Admin : UserDetail { public string AccessLevel { get; set; } } public class Resident : UserBelongsToApartmentComplex { public string Pets { get; set; } public bool HasInsurance { get; set; } public virtual IList<Lease> Leases { get; set; } public int? CurrentUnitId { get; set; } public virtual Unit CurrentUnit { get; set; } public Resident() { Leases = new List<Lease>(); } } //response data from sever from endpoint public IQueryable Residents() [{"$id":"1","$type":"RadiusBlue.Core.Models.Resident, RadiusBlue.Core","Pets":"Sadie, a westie","HasInsurance":false,"Leases":[{"$id":"2","$type":"RadiusBlue.Core.Models.Lease, RadiusBlue.Core","Start":"2012-05-23T00:00:00.000","End":"2013-05-23T00:00:00.000","UnitId":2,"Unit":{"$id":"3","$type":"RadiusBlue.Core.Models.Unit, RadiusBlue.Core","Building":"B","Floor":2,"ModelName":"Tera","RentAmount":2500.00,"NumberOfBeds":1,"NumberOfBaths":3,"UnitName":"102A","IsInactive":true,"Inhabitants":[],"ApartmentComplexId":1,"ApartmentComplex":{"$id":"4","$type":"RadiusBlue.Core.Models.ApartmentComplex, RadiusBlue.Core","Name":"The Stratford","StreetAddress":"100 S Park Ave","City":"Winter Park","StateId":10,"ZipCode":"32792","PropertyManagementCompanyId":1,"IsInactive":false,"TimeZoneId":"Eastern Standard Time","TimeZone":{"$id":"5","$type":"System.TimeZoneInfo, mscorlib","Id":"Eastern Standard Time","DisplayName":"(UTC-05:00) Eastern Time (US & Canada)","StandardName":"Eastern Standard Time","DaylightName":"Eastern Daylight Time","BaseUtcOffset":"-PT5H","AdjustmentRules":[{"$id":"6","$type":"System.TimeZoneInfo+AdjustmentRule, mscorlib","DateStart":"0001-01-01T00:00:00.000","DateEnd":"2006-12-31T00:00:00.000","DaylightDelta":"PT1H","DaylightTransitionStart":{"$id":"7","$type":"System.TimeZoneInfo+TransitionTime, mscorlib","TimeOfDay":"0001-01-01T02:00:00.000","Month":4,"Week":1,"Day":1,"DayOfWeek":"Sunday","IsFixedDateRule":false},"DaylightTransitionEnd":{"$id":"8","$type":"System.TimeZoneInfo+TransitionTime, mscorlib","TimeOfDay":"0001-01-01T02:00:00.000","Month":10,"Week":5,"Day":1,"DayOfWeek":"Sunday","IsFixedDateRule":false}},{"$id":"9","$type":"System.TimeZoneInfo+AdjustmentRule, mscorlib","DateStart":"2007-01-01T00:00:00.000","DateEnd":"9999-12-31T00:00:00.000","DaylightDelta":"PT1H","DaylightTransitionStart":{"$id":"10","$type":"System.TimeZoneInfo+TransitionTime, mscorlib","TimeOfDay":"0001-01-01T02:00:00.000","Month":3,"Week":2,"Day":1,"DayOfWeek":"Sunday","IsFixedDateRule":false},"DaylightTransitionEnd":{"$id":"11","$type":"System.TimeZoneInfo+TransitionTime, mscorlib","TimeOfDay":"0001-01-01T02:00:00.000","Month":11,"Week":1,"Day":1,"DayOfWeek":"Sunday","IsFixedDateRule":false}}],"SupportsDaylightSavingTime":true},"Users":[{"$ref":"1"}],"Groups":[],"IsArchived":false,"ApartmentComplexId":1,"Id":1},"Id":2},"ResidentId":3,"Resident":{"$ref":"1"},"LeaseStatusId":4,"LeaseStatus":{"$id":"12","$type":"RadiusBlue.Core.Models.LeaseStatus, RadiusBlue.Core","Description":"Lost","Id":4},"Id":1},{"$id":"13","$type":"RadiusBlue.Core.Models.Lease, RadiusBlue.Core","Start":"2013-05-24T00:00:00.000","End":"2014-05-24T00:00:00.000","UnitId":1,"Unit":{"$id":"14","$type":"RadiusBlue.Core.Models.Unit, RadiusBlue.Core","Building":"A","Floor":2,"ModelName":"Aqua","RentAmount":2000.00,"NumberOfBeds":2,"NumberOfBaths":1,"UnitName":"101A","IsInactive":true,"Inhabitants":[{"$ref":"1"}],"ApartmentComplexId":1,"ApartmentComplex":{"$ref":"4"},"Id":1},"ResidentId":3,"Resident":{"$ref":"1"},"LeaseStatusId":1,"LeaseStatus":{"$id":"15","$type":"RadiusBlue.Core.Models.LeaseStatus, RadiusBlue.Core","Description":"Active","Id":1},"Id":2}],"CurrentUnitId":1,"CurrentUnit":{"$ref":"14"},"ApartmentComplexId":1,"ApartmentComplex":{"$ref":"4"},"Id":3,"User":{"$id":"16","$type":"RadiusBlue.Core.Models.UserProfile, RadiusBlue.Core","UserName":"vjiawon#gmail.com","FirstName":"Vishal","LastName":"Jiawon","Age":27,"PhoneNumber":"123 456 7890","IsInactive":false,"UserDetail":{"$ref":"1"},"GroupMembers":[],"MaintenanceRequests":[],"Id":3},"IsArchived":false,"LastUpdatedById":1,"LastUpdatedDate":"0001-01-01T00:00:00.000+00:00","IsInactive":false,"CreatedById":1,"CreatedDate":"0001-01-01T00:00:00.000+00:00"}]
I do not doubt that there is a bug in BreezeJS somewhere. I can report that, at least as of v.1.3.4, Breeze can expand multiple navigation properties of a TPH class ... and not just on the first entity returned. I just modified the "can navigate to AccountType eagerly loaded with expand" test in inheritanceTests.js in DocCode so that (a) it also expands the Status navigation and (b) the tests are performed on the 3rd entity returned rather than the 1st. The query is something like this: var em = newEm(); // clean, empty EntityManager return EntityQuery.from('bankRootTPHs').take(3) .expand('AccountType, Status')) .using(em).execute().then(success).fail(handleFail); ... function success(data) { var entity = data.results[data.results.length-1]; // get the last one (the 3rd) var type = data.query.entityType.shortName; if (!entity) { ok(false, "a query failed to return a single " + type); } // more tests // I just set a breakpoint and inspected // entity.accountType() and entity.status() // Both returned the expected related entities } I see that both the related AccountType and the related Status are available from the entity. So something else is wrong. Questions about your Example First I am compelled to observe that you have a lot of expands. I count 5 related entities. That can hurt performance. I know we're not talking about that but I'm calling it out. Second, the super class UserDetail is concrete but the intermediate derived class UserBelongsToApartmentComplex is abstract. You have inheritance class hierarchies that go concrete/abstract/concrete. The queried type, Residents is one such class. And a class at every level maps to the "UserDetail" table, yes? I'm pretty sure we didn't test for that scenario ... which is pretty uncommon. I wasn't even sure that worked! For now I have to take your word for it that EF allows such a construct. It would seem that BreezeJS is confused about it. We'll take a look.
Filling child entity with Entity Framework SqlQuery
I have two entities in 1:n relationship: Category and Product. public class Category { public int CategoryID { get; set; } public string CategoryName { get; set; } public virtual ICollection<Product> Products { get; set; } } public class Product { public int ProductID { get; set; } public string ProductName { get; set; } public virtual Product { get; set; } } public class context : DbContext { public DbSet<Category> Categories { get; set; } public DbSet<Product> Products { get; set; } } Its possible to load products in every category by Eager loading. context.Categories.Include(c=>c.Products).ToList() How can I load products in every category in below query same as Eager loading? var q = #" SELECT Categories.* JOIN Products ON Category.CategoryId = Products.CategoryId"; var c = context.Categories.SqlQuery(q).ToList(); Its only a simple query. I need to use SqlQuery to execute some queries.
According to this explanation you can't: the query should be written to ensure that it only returns entities that are really of the requested type (my emphasis) So it's only by lazy loading (if enabled) that you can load the Products of the categories after the SqlQuery has run, which will cause n+1 queries.
I don't think it is possible to materialize entities obtained from Sql query if the result contains multiple entity types.