Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EF Core: Owned types support #500

Merged
merged 19 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8d6e800
Treat Owned types as Complex Objects
Daniel-Svensson Apr 22, 2024
549c964
delete old file
Daniel-Svensson Apr 22, 2024
994c2c4
Setup initial test for compelx types
Daniel-Svensson Apr 23, 2024
dbff7b2
cleanup OnModelCreating
Daniel-Svensson Apr 23, 2024
3b06161
Simplify conditional NETSTANDARD compilation
Daniel-Svensson Apr 24, 2024
1786fd6
Add OwnedTypes Scenario to NS2 and add codegen tests
Daniel-Svensson Apr 24, 2024
330dd81
be eexplicit about nestandard reference
Daniel-Svensson Apr 24, 2024
dd3e854
disable codegen test för NET472 since output is different
Daniel-Svensson Apr 24, 2024
6e789d9
Ensure AssociationAttribute name is unique if there are multiple
Daniel-Svensson Apr 24, 2024
2aecd25
remove special case of not generating Key attribute on owned entities
Daniel-Svensson Apr 24, 2024
b16cffb
Fix "updatebaselines.bat" file generation
Daniel-Svensson Apr 24, 2024
f0a808f
move blocking of "System.Runtime.CompilerServices" to CustomAttribute…
Daniel-Svensson Apr 24, 2024
8143df9
Add more (all?) variants of Owned entities
Daniel-Svensson Apr 24, 2024
ccd5d8d
Add readme file
Daniel-Svensson Jun 3, 2024
89bc591
make CreateAssociationAttribute static
Daniel-Svensson Jun 3, 2024
a47714a
Add composition to owned entities with explicit key
Daniel-Svensson Jun 3, 2024
007b061
Update version and changelog
Daniel-Svensson Jun 3, 2024
0d195f3
minor readme update
Daniel-Svensson Jun 3, 2024
14042dd
Update src/OpenRiaServices.Server.EntityFrameworkCore/Framework/READM…
Daniel-Svensson Jun 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 0 additions & 39 deletions src/FeaturePackage.targets

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, ICustom
td = new EFCoreTypeDescriptor(_typeDescriptionContext, entityType, parent);
}
#else
// TODO: Determine if we can handle Complex Types
// - it must "look the same" for all usages (be configured the same in all places to make sense)
// - Should they instead be treated as different types per usage (on the client)
if (model != null && model.FindEntityType(objectType.FullName) is IReadOnlyEntityType entityType)
{
td = new EFCoreTypeDescriptor(_typeDescriptionContext, entityType, parent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
using Microsoft.EntityFrameworkCore;
using System.Diagnostics;

#if NETSTANDARD
using IReadOnlyNavigation = Microsoft.EntityFrameworkCore.Metadata.INavigation;
using IReadOnlyEntityType = Microsoft.EntityFrameworkCore.Metadata.IEntityType;
using IReadOnlyProperty = Microsoft.EntityFrameworkCore.Metadata.IProperty;
#endif

namespace OpenRiaServices.Server.EntityFrameworkCore
{
/// <summary>
Expand Down Expand Up @@ -45,23 +51,21 @@
}

// Verify that full name is not null since Model.FindEntityType throws argument exception if full name is null
#if NETSTANDARD2_0
public IEntityType GetEntityType(Type type) => type?.FullName != null ? Model.FindEntityType(type) : null;
#else
public IReadOnlyEntityType GetEntityType(Type type) => type?.FullName != null ? ((IReadOnlyModel)Model).FindEntityType(type) : null;
#endif
public IReadOnlyEntityType GetEntityType(Type type) => type?.FullName != null ? Model.FindEntityType(type) : null;

/// <summary>
/// Creates an AssociationAttribute for the specified navigation property
/// </summary>
/// <param name="navigationProperty">The navigation property that corresponds to the association (it identifies the end points)</param>
/// <returns>A new AssociationAttribute that describes the given navigation property association</returns>
internal AssociationAttribute CreateAssociationAttribute(INavigation navigationProperty)
internal AssociationAttribute CreateAssociationAttribute(IReadOnlyNavigation navigationProperty)
{
var fk = navigationProperty.ForeignKey;

string thisKey;
string otherKey;
string name = fk.GetConstraintName();

#if NETSTANDARD2_0
if (navigationProperty.IsDependentToPrincipal())
#else
Expand All @@ -77,17 +81,26 @@

thisKey = FormatMemberList(fk.PrincipalKey.Properties);
otherKey = FormatMemberList(fk.Properties);
Debug.Assert(fk.IsOwnership == fk.DeclaringEntityType.IsOwned());

// In case there are multiple navigation properties to Owned entities
// and they have explicity defined keys (they mirror the owners key) then
// they will have the same foreign key name and we have to make them unique
if (fk.DeclaringEntityType.IsOwned())
{
name += "|owns:" + navigationProperty.Name;
}
}

var assocAttrib = new AssociationAttribute(fk.GetConstraintName(), thisKey, otherKey);
var assocAttrib = new AssociationAttribute(name, thisKey, otherKey);
assocAttrib.IsForeignKey = IsForeignKey(navigationProperty);
return assocAttrib;
}

#if NETSTANDARD2_0
private static bool IsForeignKey(INavigation navigationProperty) => navigationProperty.IsDependentToPrincipal();
private static bool IsForeignKey(IReadOnlyNavigation navigationProperty) => navigationProperty.IsDependentToPrincipal();
Fixed Show fixed Hide fixed
#else
private static bool IsForeignKey(INavigation navigationProperty) => navigationProperty.IsOnDependent;
private static bool IsForeignKey(IReadOnlyNavigation navigationProperty) => navigationProperty.IsOnDependent;
#endif


Expand All @@ -96,7 +109,7 @@
/// </summary>
/// <param name="members">A collection of members.</param>
/// <returns>A comma delimited list of member names.</returns>
protected static string FormatMemberList(IEnumerable<IProperty> members)
protected static string FormatMemberList(IEnumerable<IReadOnlyProperty> members)
{
string memberList = string.Empty;
foreach (var prop in members)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,10 @@

bool hasKeyAttribute = pd.Attributes[typeof(KeyAttribute)] != null;
var property = _entityType.FindProperty(pd.Name);
// TODO: Review all usage of isEntity to validate if we should really copy logic from EF6
bool isEntity = !_entityType.IsOwned();

if (property != null)
{
if (isEntity && property.IsPrimaryKey() && !hasKeyAttribute)
if (property.IsPrimaryKey() && !hasKeyAttribute)
Fixed Show fixed Hide fixed
{
attributes.Add(new KeyAttribute());
hasKeyAttribute = true;
Expand Down Expand Up @@ -192,7 +190,7 @@
bool isStringType = pd.PropertyType == typeof(string) || pd.PropertyType == typeof(char[]);
if (isStringType &&
pd.Attributes[typeof(StringLengthAttribute)] == null &&
property.GetMaxLength() is int maxLength)
property.GetMaxLength() is int maxLength)
{
attributes.Add(new StringLengthAttribute(maxLength));
}
Expand Down Expand Up @@ -228,33 +226,32 @@
attributes.Add(new RoundtripOriginalAttribute());
}
}
}

// Add the Editable attribute if required
if (editableAttribute != null && pd.Attributes[typeof(EditableAttribute)] == null)
{
attributes.Add(editableAttribute);
// Add the Editable attribute if required
if (editableAttribute != null && pd.Attributes[typeof(EditableAttribute)] == null)
{
attributes.Add(editableAttribute);
}
}

// Add AssociationAttribute if required for the specified property
if (isEntity
&& _entityType.FindNavigation(pd.Name) is INavigation navigation)
if (_entityType.FindNavigation(pd.Name) is { } navigation)
{
#if NETSTANDARD2_0
bool isManyToMany = navigation.IsCollection() && navigation.FindInverse()?.IsCollection() == true;
bool addAssociationAttribute = !isManyToMany;
#else
bool isManyToMany = navigation.IsCollection && navigation.Inverse?.IsCollection == true;
bool addAssociationAttribute = !isManyToMany
// Don't generate association attributes for Owned types (onless they have all FK fields explictly defined, in which case they can be treated as Entities)
// if we generate association attributes then it cannot be treated as a ComplexObject
&& !(navigation.ForeignKey.Properties.Any(static p => p.IsShadowProperty()));
#endif
if (!isManyToMany)

if (addAssociationAttribute && pd.Attributes[typeof(AssociationAttribute)] is null)
{
var assocAttrib = (AssociationAttribute)pd.Attributes[typeof(AssociationAttribute)];
if (assocAttrib == null)
{
assocAttrib = TypeDescriptionContext.CreateAssociationAttribute(navigation);
attributes.Add(assocAttrib);
}
attributes.Add(TypeDescriptionContext.CreateAssociationAttribute(navigation));
}

}

return attributes.ToArray();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
using Microsoft.EntityFrameworkCore;

#nullable enable

#if NET8_0_OR_GREATER

namespace EFCoreModels.Scenarios.ComplexTypes
{

public class Address
{
public required string AddressLine { get; set; }
public required string City { get; set; }
}

public class ContactInfo
{
public required Address Address { get; set; }

public required string HomePhone { get; set; }

public int PossibleId { get; set; }
}

public class Employee
{
public int EmployeeId { get; set; }
public required ContactInfo ContactInfo { get; set; }
}

public class ComplexTypesDbContext : DbContext
{
public ComplexTypesDbContext()
{
}

public ComplexTypesDbContext(DbContextOptions<ComplexTypesDbContext> options)
: base(options)
{
}

public DbSet<Employee> Employees { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.ComplexProperty(typeof(ContactInfo), nameof(Employee.ContactInfo), x =>
{
x.Property(nameof(ContactInfo.HomePhone)).HasMaxLength(24);
x.ComplexProperty(typeof(Address), nameof(ContactInfo.Address), address =>
{
address.Property(nameof(Address.AddressLine)).HasMaxLength(100);
address.Property(nameof(Address.City)).HasMaxLength(50);
});
})
.HasKey(e => e.EmployeeId);

base.OnModelCreating(modelBuilder);
}


protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=AdventureWorks;Integrated Security=True;MultipleActiveResultSets=True");
}
}
}
}
#endif
Loading
Loading