This post introduces the PropertySphere sample application. I’m going to talk about some aspects of the sample application in this post, then in the next one, we will introduce AI into the mix.
You can also watch me walk through the entire application in this video.
This is based on a real-world scenario from a customer. One of the nicest things about AI being so easy to use is that I can generate throwaway code for a conversation with a customer that is actually a full-blown application.
The full code for the sample application is available on GitHub.
Here is the application dashboard, so you can get some idea about what this is all about:

The idea is that you have Properties (apartment buildings), which have Units (apartments), which you then Lease to Renters. Note the capitalized words in the last sentence, those are the key domain entities that we work with.
Note that this dashboard shows quite a lot of data from many different places in the system. The map defines which properties we are looking at. It’s not just a static map, it is interactive. You can zoom in on a region to apply a spatial filter to the data in the dashboard.
Let’s take a closer look at what we are doing here. I’m primarily a backend guy, so I’m ignoring what the front end is doing to focus on the actual behavior of the system.
Here is what a typical endpoint will return for the dashboard:
[HttpGet("status/{status}")]
public IActionResult GetByStatus(string status, [FromQuery] string? boundsWkt)
{
var docQuery = _session
.Query<ServiceRequests_ByStatusAndLocation.Result,
ServiceRequests_ByStatusAndLocation>()
.Where(x => x.Status == status)
.OrderByDescending(x => x.OpenedAt)
.Take(10);
if (!string.IsNullOrWhiteSpace(boundsWkt))
{
docQuery = docQuery.Spatial(
x => x.Location, spatial => spatial.Within(boundsWkt));
}
var results = docQuery.Select(x => new
{
x.Id,
x.Status,
x.OpenedAt,
x.UnitId,
x.PropertyId,
x.Type,
x.Description,
PropertyName = RavenQuery.Load<Property>(x.PropertyId).Name,
UnitNumber = RavenQuery.Load<Unit>(x.UnitId).UnitNumber
}).ToList();
return Ok(results);
}We use a static index (we’ll see exactly why in a bit) to query for all the service requests by status and location, and then we project data from the document, including related document properties (the last two lines in the Select call).
A ServiceRequest doesn’t have a location, it gets that from its associated Property, so during indexing, we pull that from the relevant Property, like so:
Map = requests =>
from sr in requests
let property = LoadDocument<Property>(sr.PropertyId)
select new Result
{
Id = sr.Id,
Status = sr.Status,
OpenedAt = sr.OpenedAt,
UnitId = sr.UnitId,
PropertyId = sr.PropertyId,
Type = sr.Type,
Description = sr.Description,
Location = CreateSpatialField(property.Latitude, property.Longitude),
};You can see how we load the related Property and then index its location for the spatial query (last line).
You can see more interesting features when you drill down to the Unit page, where both its current status and its utility usage are displayed. That is handled using RavenDB’s time series feature, and then projected to a nice view on the frontend:

In the backend, this is handled using the following action call:
[HttpGet("unit/{*unitId}")]
public IActionResult GetUtilityUsage(string unitId,
[FromQuery] DateTime? from, [FromQuery] DateTime? to)
{
var unit = _session.Load<Unit>(unitId);
if (unit == null)
return NotFound("Unit not found");
var fromDate = from ?? DateTime.Today.AddMonths(-3);
var toDate = to ?? DateTime.Today;
var result = _session.Query<Unit>()
.Where(u => u.Id == unitId)
.Select(u => new
{
PowerUsage = RavenQuery.TimeSeries(u, "Power")
.Where(ts => ts.Timestamp >= fromDate && ts.Timestamp <= toDate)
.GroupBy(g => g.Hours(1))
.Select(g => g.Sum())
.ToList(),
WaterUsage = RavenQuery.TimeSeries(u, "Water")
.Where(ts => ts.Timestamp >= fromDate && ts.Timestamp <= toDate)
.GroupBy(g => g.Hours(1))
.Select(g => g.Sum())
.ToList()
})
.FirstOrDefault();
return Ok(new
{
UnitId = unitId,
UnitNumber = unit.UnitNumber,
From = fromDate,
To = toDate,
PowerUsage = result?.PowerUsage?.Results?
.Select(r => new UsageDataPoint
{
Timestamp = r.From,
Value = r.Sum[0],
}).ToList() ?? new List<UsageDataPoint>(),
WaterUsage = result?.WaterUsage?.Results?
.Select(r => new UsageDataPoint
{
Timestamp = r.From,
Value = r.Sum[0],
}).ToList() ?? new List<UsageDataPoint>()
});As you can see, we run a single query to fetch data from multiple time series, which allows us to render this page.
By now, I think you have a pretty good grasp of what the application is about. So get ready for the next post, where I will talk about how to add AI capabilities to the mix.







