Data Architecture & Relationships
This page explains how the Estate, Department, Employee, and Project models work together in Destinet.
Architecture Overview
Destinet uses a normalized database structure with four separate master entity collections:
┌──────────────────────────┐ ┌──────────────────────────┐
│ Departments Collection │ │ Employees Collection │
│ (Meglerkontor) │◄──────┤ (Meglere) │
│ Master Entity │ │ Master Entity │
└──────────────────────────┘ └──────────────────────────┘
↑ ↑
│ │
│ Referenced by │ Referenced by
│ │
├──────────────────────────────────┴──────────────┐
│ │
┌──────────────────────────┐ ┌──────────────────────────┐
│ Estates Collection │ │ Projects Collection │
│ (Eiendommer) │ │ (Boligprosjekter) │
│ References IDs │ │ References IDs │
└──────────────────────────┘ └──────────────────────────┘
↑ │
│ │
└──────────────────────────────────┘
(Estates can belong to Projects)
Master Entities (Stored Once, Referenced Many Times)
- Department - Real estate offices/brokerages
- Employee - Individual brokers/agents
- Project - New construction projects (apartment complexes, etc.)
- Estate - Individual properties (can be standalone or part of Project)
Why Separate Collections?
Benefits
| Benefit | Explanation |
|---|---|
| No duplication | Each department/employee is stored once, not repeated on every estate |
| Easy updates | Change employee's email once → reflected on all estates |
| Efficient queries | Find all estates for a specific employee or department |
| Data integrity | Single source of truth for each entity |
| Scalability | Supports future features (employee statistics, department dashboards, etc.) |
Example: Email Update
With duplicated data (bad):
With normalized data (good):
Employee changes email → Update 1 employee record
All 150 estates automatically show new email via reference
Shared Image Class
All entities (Estate, Department, Employee, Project, Phase, Building) use a single shared Image class instead of separate PropertyImage, DepartmentImage, EmployeeImage classes.
Benefits of Consolidation
| Benefit | Explanation |
|---|---|
| DRY Principle | Single source of truth for image handling |
| Consistency | Same behavior across all entity types |
| Easy maintenance | Update image features once, applies everywhere |
| Future features | Add thumbnails, CDN support, lazy loading once for all entities |
How It Works
The Category field provides context-specific categorization:
public class Image
{
public string Id { get; set; }
public string? OriginalUrl { get; set; }
public string Filename { get; set; }
public string FileExtension { get; set; }
public string? ExternalProviderUrl { get; set; }
public string? Caption { get; set; }
public string? AltText { get; set; }
public string? Category { get; set; } // Context-dependent!
public int? Order { get; set; }
public int? Width { get; set; }
public int? Height { get; set; }
public DateTime? LastModifiedOrigin { get; set; }
public DateTime? LastModifiedLocal { get; set; }
}
Category Examples by Entity
// Estate images
estate.Images.Add(new Image { Category = "Facade" });
estate.Images.Add(new Image { Category = "Living Room" });
estate.Images.Add(new Image { Category = "Floor Plan" });
// Department images
department.Images.Add(new Image { Category = "Office Facade" });
department.Images.Add(new Image { Category = "Team Photo" });
// Employee images
employee.Images.Add(new Image { Category = "Profile Photo" });
employee.Images.Add(new Image { Category = "Signature Image" });
// Project images
project.Images.Add(new Image { Category = "Rendering" });
project.Images.Add(new Image { Category = "Site Plan" });
How Relationships Work
Estate References
public class Estate
{
// Department/Employee References
public int? DepartmentId { get; set; } // 3005093
public List<BrokerWithRole> BrokersIdWithRoles { get; set; }
// Project References (for new builds)
public string? ProjectId { get; set; } // "PROJ-2024-001" (null if standalone)
}
public class BrokerWithRole
{
public string EmployeeId { get; set; } // Reference to Employee
public int BrokerRole { get; set; } // Role enum (primary, secondary, etc.)
}
Employee References
public class Employee
{
public List<string>? DepartmentIds { get; set; } // ["3005093", "3005094"]
// Note: Employees can belong to multiple departments
}
Department References to Employees
public class Department
{
public List<string>? ManagingDirectorIds { get; set; } // ["3006722"]
public List<string>? DepartmentManagerIds { get; set; } // ["3006723"]
public List<string>? ResponsibleBrokerIds { get; set; } // ["3006708"]
}
Project References
public class Project
{
public string? DepartmentId { get; set; } // "3005093" (Department reference)
public string? PrimaryAgentId { get; set; } // "3006722" (Employee reference)
public List<string>? AdditionalAgentIds { get; set; } // ["3006723"] (Employee references)
// Nested entities (stored within Project)
public List<Phase>? Phases { get; set; }
public List<Building>? Buildings { get; set; }
}
Looking Up Full Data
// 1. Get estate
var estate = await estateRepository.GetById("3221523");
// 2. Get related department
var department = await departmentRepository.GetById(estate.DepartmentId.ToString());
// 3. Get related employees
var employees = new List<Employee>();
foreach (var broker in estate.BrokersIdWithRoles)
{
var employee = await employeeRepository.GetById(broker.EmployeeId);
employees.Add(employee);
}
// 4. Present to user
return new EstateDetailsViewModel
{
Estate = estate,
Department = department,
Employees = employees
};
Vitec Integration: Upsert Strategy
When Vitec pushes updates, we extract and update each entity separately.
Vitec XML Structure
Vitec sends all data in one XML per estate:
<eneiendom>
<!-- Estate data -->
<felt navn="id">3221523</felt>
<felt navn="adresse">Storgata 10</felt>
<!-- Department data (embedded) -->
<felt navn="avdeling_id">3005093</felt>
<felt navn="avdeling_navn">Proaktiv Næring</felt>
<felt navn="avdeling_email">borg@proaktiv.no</felt>
<!-- Employee data (embedded) -->
<felt navn="avdeling_fagansvarlig_id">3006722</felt>
<felt navn="avdeling_fagansvarlig_navn">Karianne Jensen</felt>
<felt navn="avdeling_fagansvarlig_email">karianne@proaktiv.no</felt>
</eneiendom>
Processing Strategy
public async Task ProcessVitecEstate(VitecXml vitecData)
{
// 1. Extract and upsert Department
var department = new Department
{
Id = vitecData.AvdelingId.ToString(),
Name = vitecData.AvdelingNavn,
Email = vitecData.AvdelingEmail,
LastModifiedLocal = DateTime.UtcNow,
Origin = "Vitec"
};
await departmentRepository.Upsert(department);
// 2. Extract and upsert Primary Employee
var employee = new Employee
{
Id = vitecData.FagansvarligId,
Name = vitecData.FagansvarligNavn,
Email = vitecData.FagansvarligEmail,
DepartmentIds = new List<string> { vitecData.AvdelingId.ToString() },
LastModifiedLocal = DateTime.UtcNow,
Origin = "Vitec"
};
await employeeRepository.Upsert(employee);
// 3. Extract and upsert Additional Employees
foreach (var ansatt in vitecData.Ansatte)
{
var additionalEmployee = new Employee
{
Id = ansatt.Id,
Name = ansatt.Navn,
Email = ansatt.Email,
DepartmentIds = new List<string> { vitecData.AvdelingId.ToString() },
LastModifiedLocal = DateTime.UtcNow,
Origin = "Vitec"
};
await employeeRepository.Upsert(additionalEmployee);
}
// 4. Create/update Estate with ONLY references
var estate = new Estate
{
Id = vitecData.Id.ToString(),
DepartmentId = vitecData.AvdelingId,
BrokersIdWithRoles = new List<BrokerWithRole>
{
new BrokerWithRole
{
EmployeeId = vitecData.FagansvarligId,
BrokerRole = 1 // Primary broker
}
},
// Add additional brokers
// BrokersIdWithRoles.AddRange(vitecData.Ansatte.Select(a =>
// new BrokerWithRole { EmployeeId = a.Id, BrokerRole = 2 })),
// ... other estate properties
Origin = "Vitec"
};
await estateRepository.Upsert(estate);
}
Update Scenarios
Scenario 1: New Estate from Vitec
{
"eventType": "estate.created",
"estateId": "3221523",
"departmentId": "3005093",
"employeeId": "3006722"
}
Destinet actions: 1. Check if Department 3005093 exists → No → Create from Vitec data 2. Check if Employee 3006722 exists → No → Create from Vitec data 3. Create Estate with references
Result: 3 new records (1 department, 1 employee, 1 estate)
Scenario 2: Employee Email Update
{
"eventType": "employee.updated",
"employeeId": "3006722",
"changes": {
"email": "new.email@proaktiv.no"
}
}
Destinet actions: 1. Update Employee 3006722 email field 2. Done!
Result: 1 updated record. All estates automatically reflect new email.
Scenario 3: Estate Updated (same department/employee)
Destinet actions: 1. Check if Department still exists → Yes → Skip 2. Check if Employee still exists → Yes → Skip 3. Update Estate price
Result: 1 updated record (estate only)
Database Schema
Departments Collection
| Field | Type | Description |
|---|---|---|
Id |
string | Unique identifier (from Vitec: avdeling_id) |
Name |
string | Department name |
RegistrationNumber |
string? | Organization number |
IsRegion |
bool? | Whether this is a regional office |
IsPartOfFranchise |
bool? | Whether this is part of a franchise |
SubDepartments |
List |
Sub departments as enum list |
Address |
Address? | Office address (Street, City, PostalCode, etc.) |
Phone |
string? | Contact phone |
Email |
string? | Contact email |
LeadsEmail |
string? | Email for leads |
Website |
string? | Department website |
Description |
string? | Description/biography of the department |
ShortDescription |
string? | Short description of the department |
LogoUrl |
string? | Logo image URL |
VideoUrl |
string? | URL to department video presentation |
HeaderImageUrl |
string? | URL to header/banner image |
PriceListUrl |
string? | URL to price list document |
ManagingDirectorIds |
List |
References to managing directors |
DepartmentManagerIds |
List |
References to department managers |
ResponsibleBrokerIds |
List |
References to responsible brokers |
Images |
ImageCollection? | Office photos (facade, interior, etc.) |
ImageTimestamp |
DateTime? | Image last modified timestamp |
IsActive |
bool? | Whether this department is currently active |
LastModifiedOrigin |
DateTime? | Last update timestamp in origin system |
LastModifiedLocal |
DateTime? | Last update timestamp locally |
OriginalDepartmentId |
string? | Old department IDs if needing to match new Id with old |
Origin |
string? | Source (e.g., "Vitec") |
CustomData |
Dictionary |
System-specific metadata |
Employees Collection
| Field | Type | Description |
|---|---|---|
Id |
string | Unique identifier (from Vitec: fagansvarlig_id, ansatte_id) |
Name |
string? | Employee name |
Title |
string? | Job title |
Email |
string? | Contact email |
MobilePhone |
string? | Mobile number |
Phone |
string? | Office phone |
PhotoUrl |
string? | Profile photo URL |
VideoUrl |
string? | URL to employee's video presentation |
Description |
string? | Biography/description |
ShortDescription |
string? | Short biography |
Qualifications |
string? | Certifications and qualifications |
IsProjectBroker |
bool? | Specializes in new build projects |
IsRentalBroker |
bool? | Specializes in rentals |
IsActive |
bool? | Active status |
DepartmentIds |
List |
References to Departments (plural - can belong to multiple) |
SoldProperties |
List |
List of sold property addresses |
Images |
ImageCollection? | Profile photos, signatures, etc. |
ImageTimestamp |
DateTime? | Image last modified timestamp |
LastModifiedOrigin |
DateTime? | Last update timestamp in origin system |
LastModifiedLocal |
DateTime? | Last update timestamp locally |
OriginalEmployeeId |
string? | Old employee IDs if needing to match new Id with old |
Origin |
string? | Source (e.g., "Vitec") |
CustomData |
Dictionary |
System-specific metadata |
Estates Collection
| Field | Type | Description |
|---|---|---|
Id |
string | Unique identifier |
AssignmentNum |
string? | Assignment number / Oppdragsnummer |
Heading |
string? | Marketing headline |
DepartmentId |
int? | Reference to Department |
BrokersIdWithRoles |
List |
References to Employees with roles |
EstateBaseType |
int? | Base type enum from Vitec (0=NotSet, 1=Detached, etc.) |
AssignmentTypeGroup |
int? | Assignment type enum from Vitec (0=NotSet, 1=Sale, etc.) |
Ownership |
int? | Ownership type enum from Vitec (0=Owned, 1=Cooperative, etc.) |
Status |
int | Status enum from Vitec (0=Request, 1=Preparation, 2=ForSale, etc.) |
Address |
Address? | Property address |
GeoCoordinates |
GeoCoordinates? | Latitude and longitude |
Matrikkel |
List |
Norwegian cadastral information |
EstateSize |
EstateSize? | Area measurements (primary room area, gross area, etc.) |
EstatePrice |
EstatePrice? | Pricing information (asking price, costs, fees, etc.) |
PartOwnership |
PartOwnership? | Cooperative/condominium details |
Images |
ImageCollection? | Property photos (facade, interior, floor plans) |
Documents |
DocumentCollection? | PDFs, prospectuses, etc. |
Links |
List |
External links (virtual tours, videos) |
Showings |
List |
Scheduled viewings |
DescriptionSections |
List |
Structured descriptions |
ProjectId |
string? | Reference to Project (null if standalone) |
Origin |
string? | Source (e.g., "Vitec") |
CustomData |
Dictionary |
System-specific metadata |
Projects Collection
| Field | Type | Description |
|---|---|---|
Id |
string | Unique identifier |
ParentProjectId |
string? | Reference to parent project if this is a sub-project |
Name |
string | Project name |
Developer |
string? | Developer company |
DeveloperWebsite |
string? | Developer company website |
SellerId |
string? | Seller identifier |
Address |
Address? | Project address |
Coordinates |
GeoCoordinates? | Latitude and longitude |
TotalUnits |
int? | Total number of units in project |
Phases |
List |
Construction/sales phases (nested) |
Buildings |
List |
Individual buildings (nested) |
Images |
ImageCollection? | Project renderings, site plans |
Documents |
DocumentCollection? | Brochures, legal documents |
DescriptionSections |
List |
Structured descriptions |
ExternalPlatformUrls |
ExternalPlatformUrls? | External platform URLs |
DepartmentId |
string? | Reference to Department handling project |
PrimaryAgentId |
string? | Reference to primary Employee |
AdditionalAgentIds |
List |
References to additional Employees |
LastModifiedOrigin |
DateTime? | Last update timestamp in origin system |
LastModifiedLocal |
DateTime? | Last update timestamp locally |
Origin |
string? | Source (e.g., "Vitec") |
CustomData |
Dictionary |
System-specific metadata |
Shared Classes Used Across All Models
| Class | Used By | Description |
|---|---|---|
Address |
All entities | Street, City, PostalCode, Municipality, County, etc. |
GeoCoordinates |
Estate, Project | Latitude, Longitude for map display |
Image |
All entities | Generic image class (facade, interior, profile photos, etc.) |
PropertyDocument |
Estate, Project | PDFs, floor plans, brochures |
PropertyLink |
Estate | External links (virtual tours, videos) |
Showing |
Estate | Scheduled open houses |
DescriptionSection |
Estate, Project | Structured description sections |
ExternalPlatformUrls |
Estate, Project | External platform URLs |
Matrikkel |
Estate | Norwegian cadastral information |
Estate-Specific Classes
| Class | Description |
|---|---|
BrokerWithRole |
Employee reference with role assignment |
EstateSize |
Area measurements (primary room area, gross area, usable area, etc.) |
EstatePrice |
Comprehensive pricing (asking price, costs, fees, collective debt, etc.) |
PartOwnership |
Cooperative/condominium details (for Borettslag) |
Plot |
Plot/land information |
EnergyRating |
Energy efficiency rating (A-G) |
Project-Specific Classes
| Class | Description |
|---|---|
Phase |
Construction/sales phases with timeline and unit counts |
Building |
Individual buildings within a project |
Query Examples
Get All Estates for an Employee
var estates = await estateRepository.Find(e =>
e.BrokersIdWithRoles.Any(b => b.EmployeeId == "3006722")
);
Get All Estates for a Department
Get Estate with Full Details
// Get estate
var estate = await estateRepository.GetById("3221523");
// Enrich with related data
var estateDetails = new EstateDetailsViewModel
{
Estate = estate,
Department = await departmentRepository.GetById(estate.DepartmentId.ToString()),
Employees = await GetEmployeesForEstate(estate)
};
async Task<List<Employee>> GetEmployeesForEstate(Estate estate)
{
var employees = new List<Employee>();
foreach (var broker in estate.BrokersIdWithRoles)
{
var employee = await employeeRepository.GetById(broker.EmployeeId);
employees.Add(employee);
}
return employees;
}
Get Employee Statistics
// Find all estates for employee
var employeeEstates = await estateRepository.Find(e =>
e.BrokersIdWithRoles.Any(b => b.EmployeeId == employeeId)
);
// Calculate statistics
var stats = new EmployeeStatistics
{
TotalListings = employeeEstates.Count,
ActiveListings = employeeEstates.Count(e => e.Status == 2), // ForSale
SoldListings = employeeEstates.Count(e => e.Status == 3), // Sold
TotalValue = employeeEstates.Sum(e => e.EstatePrice?.PriceSuggestion ?? 0)
};
Get All Estates in a Project
// Get the project
var project = await projectRepository.GetById("PROJ-2024-001");
// Get all estates belonging to this project
var projectEstates = await estateRepository.Find(e =>
e.ProjectId == "PROJ-2024-001"
);
// Count sold vs available
var soldCount = projectEstates.Count(e => e.Status == 3); // Sold
var availableCount = projectEstates.Count(e => e.Status == 2); // ForSale
Get Estates in a Specific Building
// Get project first
var project = await projectRepository.GetById("PROJ-2024-001");
var building = project.Buildings.FirstOrDefault(b => b.Id == "BYGG-A");
// Get estates in this project - would need additional filtering logic
var projectEstates = await estateRepository.Find(e =>
e.ProjectId == "PROJ-2024-001"
// Note: Building linkage would need additional implementation
);
Get All Departments Serving a Postal Code
Get All Employees Working at Multiple Departments
Best Practices
DO:
- Always use IDs for references between entities
- Upsert departments and employees before creating/updating estates
- Query by ID when looking up related entities
- Cache frequently accessed department/employee data if needed for performance
- Index DepartmentId and BrokersIdWithRoles.EmployeeId fields in Estate collection
- Store integer enums directly from source system (Vitec) - use mapping tables for display
DON'T:
- Store full Department/Employee objects in Estate
- Duplicate department/employee data across estates
- Skip upserting department/employee when processing estate updates
- Assume department/employee exists without checking
- Convert enums to strings during storage - keep as integers