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 3 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 @@ -245,8 +245,18 @@ protected override IEnumerable<Attribute> GetMemberAttributes(PropertyDescriptor
#else
bool isManyToMany = navigation.IsCollection && navigation.Inverse?.IsCollection == true;
#endif

#if NETSTANDARD2_0
if (!isManyToMany)
{
#else
if (!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.TargetEntityType.IsOwned() && navigation.ForeignKey.Properties.Any(static p => p.IsShadowProperty())))
{
#endif

var assocAttrib = (AssociationAttribute)pd.Attributes[typeof(AssociationAttribute)];
if (assocAttrib == null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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)
{
// add concurrencymode = Fixed on all "Owned types" ??

modelBuilder.Owned<Address>();
//modelBuilder.Owned<ContactInfo>();
Daniel-Svensson marked this conversation as resolved.
Show resolved Hide resolved

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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.OwnedTypes
{

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; }
}

// TODO: Owned typed with "backwards" FK
public class OwnedTypesDbContext : DbContext
{
public OwnedTypesDbContext()
{
}

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

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

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// add concurrencymode = Fixed on all "Owned types" ??

modelBuilder.Owned<Address>();
//modelBuilder.Owned<ContactInfo>();

modelBuilder.Entity<Employee>()
.OwnsOne(typeof(ContactInfo), nameof(Employee.ContactInfo), x =>
{
x.Property(nameof(ContactInfo.HomePhone)).HasMaxLength(24);
x.OwnsOne(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
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using OwnedTypes = EFCoreModels.Scenarios.OwnedTypes;
using ComplexTypes = EFCoreModels.Scenarios.ComplexTypes;
using MSTest = Microsoft.VisualStudio.TestTools.UnitTesting;

namespace OpenRiaServices.Server.EntityFrameworkCore.Test
{
[TestClass]
public class DomainServiceDescriptionTests
{
/// <summary>
/// Verify that the EF metadata provider is registered for mapped CTs, and that attribute are inferred properly
/// </summary>
[TestMethod]
public void ComplexType_EFCore_OwnedTypes()
{
DomainServiceDescription dsd = DomainServiceDescription.GetDescription(typeof(EFCoreOwnedTypesService));

CollectionAssert.AreEquivalent(dsd.EntityTypes.ToList(), new[] { typeof(OwnedTypes.Employee) });
CollectionAssert.AreEquivalent(dsd.ComplexTypes.ToList(), new[] { typeof(OwnedTypes.Address), typeof(OwnedTypes.ContactInfo) });

var employee = TypeDescriptor.GetProperties(typeof(OwnedTypes.Employee));
Assert.IsNotNull(employee[nameof(OwnedTypes.Employee.EmployeeId)]!.Attributes[typeof(KeyAttribute)]);

// the HomePhone member is mapped as non-nullable, with a max length of 24. Verify attributes
// were inferred
var contactInfo = TypeDescriptor.GetProperties(typeof(OwnedTypes.ContactInfo));
var homePhone = contactInfo[nameof(OwnedTypes.ContactInfo.HomePhone)]!;
Assert.IsNotNull(homePhone.Attributes[typeof(RequiredAttribute)]);
StringLengthAttribute sl = (StringLengthAttribute)homePhone.Attributes[typeof(StringLengthAttribute)]!;
Assert.AreEqual(24, sl.MaximumLength);


// the AddressLine1 member is mapped as non-nullable, with a max length of 100. Verify attributes
// were inferred
var address = TypeDescriptor.GetProperties(typeof(OwnedTypes.Address));
var addressLine = address[nameof(OwnedTypes.Address.AddressLine)]!;
Assert.IsNotNull(addressLine.Attributes[typeof(RequiredAttribute)]);
sl = (StringLengthAttribute)addressLine.Attributes[typeof(StringLengthAttribute)]!;
Assert.AreEqual(100, sl.MaximumLength);
}
/// <summary>
/// Verify that the EF metadata provider is registered for mapped CTs, and that attribute are inferred properly
/// </summary>
[TestMethod]
[MSTest.Ignore("Does not work yet, attributes are not discovered on ComplexObjects")]
public void ComplexType_EFCore_ComplexTypes()
{
DomainServiceDescription dsd = DomainServiceDescription.GetDescription(typeof(EFCoreComplexTypesService));

CollectionAssert.AreEquivalent(dsd.EntityTypes.ToList(), new[] { typeof(ComplexTypes.Employee) });
CollectionAssert.AreEquivalent(dsd.ComplexTypes.ToList(), new[] { typeof(ComplexTypes.Address), typeof(ComplexTypes.ContactInfo) });

var employee = TypeDescriptor.GetProperties(typeof(ComplexTypes.Employee));
Assert.IsNotNull(employee[nameof(ComplexTypes.Employee.EmployeeId)]!.Attributes[typeof(KeyAttribute)]);

// the HomePhone member is mapped as non-nullable, with a max length of 24. Verify attributes
// were inferred
var contactInfo = TypeDescriptor.GetProperties(typeof(ComplexTypes.ContactInfo));
var homePhone = contactInfo[nameof(ComplexTypes.ContactInfo.HomePhone)]!;

Assert.IsNotNull(homePhone.Attributes[typeof(RequiredAttribute)]);
StringLengthAttribute sl = (StringLengthAttribute)homePhone.Attributes[typeof(StringLengthAttribute)]!;
Assert.AreEqual(24, sl.MaximumLength);


// the AddressLine1 member is mapped as non-nullable, with a max length of 100. Verify attributes
// were inferred
var address = TypeDescriptor.GetProperties(typeof(ComplexTypes.Address));
var addressLine = address[nameof(ComplexTypes.Address.AddressLine)]!;
Assert.IsNotNull(addressLine.Attributes[typeof(RequiredAttribute)]);
sl = (StringLengthAttribute)addressLine.Attributes[typeof(StringLengthAttribute)]!;
Assert.AreEqual(100, sl.MaximumLength);
}
}

[EnableClientAccess]
public class EFCoreOwnedTypesService : DbDomainService<OwnedTypes.OwnedTypesDbContext>
{
public IQueryable<OwnedTypes.Employee> GetCustomers()
{
return null!;
}
}

[EnableClientAccess]
public class EFCoreComplexTypesService : DbDomainService<ComplexTypes.ComplexTypesDbContext>
{
public IQueryable<ComplexTypes.Employee> GetCustomers()
{
return null!;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Framework\OpenRiaServices.Server.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\DbContextModel\EFCoreModels.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions src/RiaServices.sln
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenRiaservices.EndToEnd.Wc
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenRiaservices.EndToEnd.AspNetCore.Test", "Test\OpenRiaservices.EndToEnd.AspNetCore.Test\OpenRiaservices.EndToEnd.AspNetCore.Test.csproj", "{17F902F4-7254-4FB9-9AF6-B3291B129B45}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenRiaServices.Server.EntityFrameworkCore.Test", "OpenRiaServices.Server.EntityFrameworkCore\Test\OpenRiaServices.Server.EntityFrameworkCore.Test\OpenRiaServices.Server.EntityFrameworkCore.Test.csproj", "{76DBD075-9DC2-4583-8BAB-2713E60F555B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -377,6 +379,10 @@ Global
{17F902F4-7254-4FB9-9AF6-B3291B129B45}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17F902F4-7254-4FB9-9AF6-B3291B129B45}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17F902F4-7254-4FB9-9AF6-B3291B129B45}.Release|Any CPU.Build.0 = Release|Any CPU
{76DBD075-9DC2-4583-8BAB-2713E60F555B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76DBD075-9DC2-4583-8BAB-2713E60F555B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76DBD075-9DC2-4583-8BAB-2713E60F555B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76DBD075-9DC2-4583-8BAB-2713E60F555B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -449,6 +455,7 @@ Global
{27DB096E-8794-4B9A-B3D0-AF74926DF9FD} = {BB0E26DD-14D3-4CD5-8F2B-B5C1CA281719}
{9428260A-EA94-430D-9A1A-CB9830B9E8EE} = {27DB096E-8794-4B9A-B3D0-AF74926DF9FD}
{17F902F4-7254-4FB9-9AF6-B3291B129B45} = {27DB096E-8794-4B9A-B3D0-AF74926DF9FD}
{76DBD075-9DC2-4583-8BAB-2713E60F555B} = {E8CAC11B-50B6-4243-A12A-D831EBB064CB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C135CEC9-180C-4B67-92AC-3342375AE14C}
Expand Down
Loading