What are your methods to deal with the communication of an admin panel with a domain in the case of changing values of properties of an entity without breaking the encapsulation?
public class Book : Entity {
public Book(string title, string author, string description, decimal price, short publicationYear) {
Title = title;
Author = author;
Description = description;
Price = price;
PublicationYear = publicationYear;
}
public string Title { get; private set; }
public string Author { get; private set; }
public string Description { get; private set; }
public decimal Price { get; private set; }
public short PublicationYear { get; private set; }
}
The only way to not break encapsulation is to include some parts of the presentation logic into the object itself. Not the details, mind you, but the parts which are highly coupled to this object.
I would do something like this (pseudo-code):
public class Book {
public Book(...) {
...
}
public InputComponent<Book> createAdminView() {
return new FormGroup<Book>(
new TextInput(title),
new TextInput(author),
...);
}
}
This way there is no need to publish any of the internal data fields of the object, nobody needs to know how to book looks like, and all changes related to the object will be localized.
In fact, I've been doing this for a couple for years now, and this design results in much easier to maintain code. Have a look at my presentation about Object-Oriented Domain-Driven Design to find out more: https://speakerdeck.com/robertbraeutigam/object-oriented-domain-driven-design
I've been working on a MVC5 website to manage insurance policies, and am stuck on what I think is a design problem. I have a code first, TPH entity situation with the following abstract class called policy:
public abstract class Policy
{
public int PolicyID { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime Inception { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime Expiration { get; set; }
}
And two concrete classes as follows:
public class UmbrellaPolicy : Policy
{
[Display(Name = "Umbrella:")]
public bool hasUmb { get; set; }
public virtual List<Umb> UmbCoverages { get; set; }
}
public class PackagePolicy : Policy
{
[Display(Name = "General Liability:")]
public bool hasGen { get; set; }
public virtual List<Gen> GenCoverages { get; set; }
[Display(Name = "Umbrella:")]
public bool hasUmb { get; set; }
public virtual List<Umb> UmbCoverages { get; set; }
}
And, finally a class which has a one to many relationship with both of the concrete classes above:
public class Umb
{
public int UmbID { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}", ApplyFormatInEditMode = true)]
[Display(Name = "Coverage Effective")]
public DateTime DateAdded { get; set; }
[Display(Name = "Each Occurrence Limit")]
public decimal OccurrenceLimit { get; set; }
[Display(Name = "Each Occurrence Retention")]
public decimal OccurrenceRetention { get; set; }
[Display(Name = "Aggregate Limit")]
public decimal AggregateLimit { get; set; }
public virtual Policy Policy { get; set; }
}
The business requirement is if a customer wants umbrella coverage, they purchase either an umbrella policy if they want only umbrella coverage or a package policy if they want umbrella coverage and liability coverage (like those commercials that want you to bundle homeowners and auto policies). The class that describes how much insurance a customer has (the class Umb) is the same regardless of whether the customer purchased umbrella coverage in a stand-alone policy or a package.
When I try to create a new Umb and add the policy (whether umbrella or package) to the virtual Policy property, and then try to view a list of Umb objects from the list on the concrete policy class, none show up.
After looking in the database, it appears that when I did my db migration there are three key columns: UmbrellaPolicy_PolicyID, PackagePolicy_PolicyID, and Policy_PolicyID. It successfully adds the correct policy id to the Policy_PolicyID column, but I guess entity framework looks at the Umbrella or Package column depending on which concrete class I am working with (i.e. in a strongly typed view). I feel like I'm missing something obvious or I went down a shaky implementation path. Any suggestions or readings someone could point me to would be awesome. Thanks!
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.
I see these types of model is many samples online.
public class User
{
public long Id { get; set; }
public string Name{ get; set; }
public virtual ICollection<Product> Products { get; set; }
}
Is it considered a good practice to instantiate a collection in the constructor like the code below? If so what are the reasons? How about objects in the model?
public class User
{
public User()
{
Products = new List<Product>();
}
public long Id { get; set; }
public string Name{ get; set; }
public virtual ICollection<Product> Products { get; set; }
}
Well, I would say it depends on the situation, but Products in this case would be filled from the database, via a repository, so most probably ORM of some sort, so no initialization to new List would be needed in the constructor. The meaning of null for Products is indicative that the list isn't loaded yet. On the other hand, let's say that your object must have this collection initialized. For simple objects DDD says constructors are perfectly fine to to these things, but in case of complex objects, move the construction to the Factory.