Entity Framework Core Owned Types explained

Owned entity was made available from EF Core 2.0 onwards. The same .NET type can be shared among different entities. Owned entities would not have a key or identity property of their own, but would always be a navigational property of another entity. In DDD we could see this as a value/complex type. For those coming from EF 6, you may see a similarity with complex types in your model. But the way it works and behaves in EF Core is different. There are some gotchas you need to watch out for. We’ll explore these in detail here.

Let us work with a model shown below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Student
{
public int Id { get; set; }

public string Name { get; set; }

public Address Home { get; set; }
}

public class Address
{
public string Street { get; set; }

public string City { get; set; }
}

Here Student owns Address which is the owned type and does not have its own identity property. Address becomes a navigation property on Student and would always have an one-to-one relationship (at least for now).

The DbContext would be defined like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StudentContext : DbContext
{
public DbSet<Student> Students { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.OwnsOne(s => s.Home);
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=StudentDb; Trusted_Connection=True;App=StudentContext");
optionsBuilder.EnableSensitiveDataLogging();
}
}

An owned type cannot have a DbSet<> and OnModelCreating you can specify the Home property as Owned Entity of Student.

Home would be mapped to the same table as Student.

Let us fire up this model and see it working.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.Logging;

class Program
{
static void Main(string[] args)
{
var _context = new StudentContext();
_context.GetService<ILoggerFactory>().AddConsole();
_context.Database.EnsureDeleted();
_context.Database.EnsureCreated();

InsertStudent(_context);
}

private static void InsertStudent(StudentContext context)
{
var student = new Student
{
Name = "Student_1",
Home = new Address
{
Street = "Circular Quay",
City = "Sydney"
}
};
context.Students.Add(student);
context.SaveChanges();
}
}

I have added Microsoft.EntityFrameworkCore.SqlServer and Microsoft.Extensions.Logging.Console packages.
From the console logs we see that we have Students table created and a row inserted.

1
2
3
4
5
6
7
CREATE TABLE [Students] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
[Home_City] nvarchar(max) NULL,
[Home_Street] nvarchar(max) NULL,
CONSTRAINT [PK_Students] PRIMARY KEY ([Id])
);

To query, just get the students and the owned entity is also included.

1
var students = _context.Students.ToList();

We can also store Address in another table, which we can’t do with complex types in EF6. Simply call .ToTable() and provide a different name.

1
2
3
modelBuilder.Entity<Student>()
.OwnsOne(s => s.Home)
.ToTable("HomeAddress");

Now when you run the app, you would see 2 tables being created. Note the identity column for the HomeAddress table. It is referencing Students table’s identity.

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE [Students] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
CONSTRAINT [PK_Students] PRIMARY KEY ([Id])
);
CREATE TABLE [HomeAddress] (
[StudentId] int NOT NULL,
[City] nvarchar(max) NULL,
[Street] nvarchar(max) NULL,
CONSTRAINT [PK_HomeAddress] PRIMARY KEY ([StudentId]),
CONSTRAINT [FK_HomeAddress_Students_StudentId] FOREIGN KEY ([StudentId]) REFERENCES [Students] ([Id]) ON DELETE CASCADE
);

You can ignore properties which you do not want EF tracking.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Address
{
public string Street { get; set; }

public string City { get; set; }

public string State { get; set; } // ignore this
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.OwnsOne(s => s.Home, (h) =>
{
h.Ignore(a => a.State);
h.ToTable("HomeAddress");
});
}

There are certain elements to keep in mind especially with change tracking. With EF core do not assume the same code of EF6 would give you similar behaviour. This is especially true with Change Tracking. In my view these changes are welcome and makes tracking more intuitive and easy to navigate.

When you use Add, Attach, Update or Remove on either DbSet<> or through DbContext, it effects all reachable entities. Here is what it would look like:

1
context.Students.Add(student);

This would also mark Address in a Added state.
But if you do not want to track all the entities in the graph:

1
context.Entry(student).State = EntityState.Added;

When you do this only the student is marked for insert and address is not. So how do you only change the state of address?

1
2
var address = _context.Entry(student).Reference(s => s.Home).TargetEntry;
address.State = EntityState.Unchanged;

When you mark an entity in the graph for update, all the properties are marked for update. In a disconnected (n-tier) scenario, you would need to track changes on your entity externally and let EF know about the changes. You need to have the original state of the entity and do some processing to know which properties were changed. Or you could go back to the database, get the entity and compare it’s state.

1
2
3
4
var entry = _context.Attach(student);
var dbValues = entry.GetDatabaseValues(); // gets only the student
entry.OriginalValues.SetValues(dbValues);
_context.SaveChanges();

This would only update those columns which had any changes on them. But it would only affect the student object and not the address. The address would still be in an Unchanged state. The above entry.GetDatabaseValues() would fetch only student values and not address. For you to track changes on address, you would need to explicitly check on its entity.

1
2
3
4
5
var entry = _context.Attach(student);
var adEntry = _context.Entry(student.Home);
adEntry.OriginalValues.SetValues(adEntry.GetDatabaseValues()); // gets home address
entry.OriginalValues.SetValues(entry.GetDatabaseValues()); // gets student
_context.SaveChanges();

Now on SaveChanges(), it would issue update on Address too if it found any changes.