Skip to content

Commit

Permalink
EF Core: Owned types support (#500)
Browse files Browse the repository at this point in the history
Initial support for Owned Entities for one-to-one navigation properties (EF Core: Owned types support #500)
* Owned entities without explicit keys are mapped to OpenRiaServices's [Complex Types]
* Owned entities with explicit keys are generated as normal "Entities" but are automatically annotated with [Composition]
* EFCore "Complex Types" introduced in EFCore 8.0 does not have any special handling, but works
Add package README to OpenRiaServices.Server.EntityFrameworkCore
* Update version and changelog
  • Loading branch information
Daniel-Svensson authored Jun 5, 2024
1 parent 3fc4bbf commit 34d62dd
Show file tree
Hide file tree
Showing 22 changed files with 2,959 additions and 75 deletions.
17 changes: 17 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# 5.4.4 / EF Core 3.1.0

### EF Core 3.1.0
* Initial support for Owned Entities for one-to-one navigation properties (#500)
* Owned entities without explicit keys are mapped to OpenRiaServices's [Complex Types]
* Owned entities with explicit keys are generated as normal *"Entities"* but are automatically annotated with `[Composition]`
* EFCore "Complex Types" introduced in EFCore 8.0 does not have any special handling
* Add new helper method `AttachAsModified<TEntity>(TEntity entity)` (#506)
* It works similar to the existing `AttachAsModified` extension methods on `DbSet` but
* is smarter (works with and without original entity)
* reduces code that needs to be written and works both with and without "OriginalEntity" (`RoundTripAttribute`)
* Add package README to `OpenRiaServices.Server.EntityFrameworkCore`

### Code generation
* Log whole Exceptions in DomainServiceCatalog instead of just message (#502), for better error messages on code generation failure
* Call "dotnet CodeGenTask.dll" instead of "CodeGenTask.exe" #503

# EF Core 3.0.0

* Target EF Core 8
Expand Down
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 @@ public IModel Model
}

// 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 static 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 @@ internal AssociationAttribute CreateAssociationAttribute(INavigation navigationP

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();
#else
private static bool IsForeignKey(INavigation navigationProperty) => navigationProperty.IsOnDependent;
private static bool IsForeignKey(IReadOnlyNavigation navigationProperty) => navigationProperty.IsOnDependent;
#endif


Expand All @@ -96,7 +109,7 @@ internal AssociationAttribute CreateAssociationAttribute(INavigation navigationP
/// </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 @@ protected override IEnumerable<Attribute> GetMemberAttributes(PropertyDescriptor

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)
{
attributes.Add(new KeyAttribute());
hasKeyAttribute = true;
Expand Down Expand Up @@ -192,7 +190,7 @@ protected override IEnumerable<Attribute> GetMemberAttributes(PropertyDescriptor
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,39 @@ protected override IEnumerable<Attribute> GetMemberAttributes(PropertyDescriptor
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)
{
var assocAttrib = (AssociationAttribute)pd.Attributes[typeof(AssociationAttribute)];
if (assocAttrib == null)
if (pd.Attributes[typeof(AssociationAttribute)] is null)
attributes.Add(EFCoreTypeDescriptionContext.CreateAssociationAttribute(navigation));
#if NET
if (navigation.TargetEntityType.IsOwned() && pd.Attributes[typeof(CompositionAttribute)] is null)
{
assocAttrib = TypeDescriptionContext.CreateAssociationAttribute(navigation);
attributes.Add(assocAttrib);
attributes.Add(new CompositionAttribute());
}
#endif
}

}

return attributes.ToArray();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
<TargetFrameworks>net8.0;net6.0;netstandard2.0</TargetFrameworks>
<DefineConstants>$(DefineConstants);SERVERFX;EFCORE</DefineConstants>
<GeneratePackageOnBuild Condition="'$(Configuration)'=='Release'">true</GeneratePackageOnBuild>
<VersionPrefix>3.0.0</VersionPrefix>
<VersionPrefix>3.1.0</VersionPrefix>
<AssemblyVersion>3.0.0</AssemblyVersion>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseExpression></PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
Expand Down Expand Up @@ -41,6 +42,7 @@
</ItemGroup>
<ItemGroup>
<None Include="LICENSE.md" Pack="true" PackagePath="\" />
<Content Include="README.md" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="DbResource.resx">
Expand Down
83 changes: 83 additions & 0 deletions src/OpenRiaServices.Server.EntityFrameworkCore/Framework/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine)


## TERM OF USE

By using this project or its source code, for any purpose and in any shape or form, you grant your **agreement** to all the following statements:

- You **condemn Russia and its military aggression against Ukraine**
- You **recognize that Russia is an occupant that unlawfully invaded a sovereign state**
- You **support Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas**
- You **do not support the Russian invasion or contribute to its propaganda'**

This excludes usage by the Russian state, Russian state-owned companies, Russian education who spread propaganda instead of truth, anyone who work with the *filtration camps*, or finance the war by importing Russian oil or gas.

- You allow anonymized telemetry to collected and sent during the preview releases to gather feedback about usage



## Getting Started

1. Ensure you have setup and configured `OpenRiaServices.Hosting.AspNetCore`
2. Add a reference to *OpenRiaServices.Server.EntityFrameworkCore*
`dotnet add package OpenRiaServices.Server.EntityFrameworkCore`
3. Add one or more domainservices.
Given that you have a Ef Core model named `MyDbContext` with an entity `MyEntity` you add CRUD methods using something similar to below

```csharp
using using Microsoft.EntityFrameworkCore;
using OpenRiaServices.Server.EntityFrameworkCore;

[EnableClientAccess]
public class MyDomainService : DbDomainService<MyDbContext>
{
// Not required: But it is generally a good idea to allow dependency injection of DbContext
public MyDomainService(MyDbContext dbContext)
: base(context)
{ }

/* Example of CUD methods */
public void InsertMyEntity(MyEntity entity)
=> DbContext.Entry(entity).State = EntityState.Added; // Or DbContext.Add, but it might add related entities differently
public void UpdateMyEntity(MyEntity entity)
=> base.AttachAsModified(entity); // This sets state to Modified and set modified status on individual properties based on client changes and if `RountTripOriginal` attribute is specified or not
public void DeleteMyEntity(MyEntity entity)
=> DbContext.Entry(entity).State = EntityState.Deleted;

/* Query:
* Return IQueryable<> to automatically allow the client to add filtering, paging etc.
* The queries are performed async for best performance
*/
[Query]
IQueryable<MyEntity> GetMyEntities()
=> DbContext.MyEntities.OrderBy(x => x.Id); // Sort by id to get stable Skip/Take if client does paging
}
```
4. Ensure that `MyDomainService` is mapped, (See [Setup instructions in OpenRiaServices.Hosting.AspNetCore readme](https://www.nuget.org/packages/OpenRiaServices.Hosting.AspNetCore))


## Owned Entities

* *one-to-one* relations using Owned Entities are fully supported
* Any EF Core configuration/metadata applied to the owned entity (such as Required, MaxLength etc) are part of generated client Code
* *one-to-many* relations might work, but it has not been verified at the moment

### Owned Entities without explicit key
EF Core owned entities are mapped to OpenRiaServices's [Complex Types](https://openriaservices.gitbook.io/openriaservices/ee707348/ee707356/gg602753) as long as the owned entity does not have have an explicit key.


### Owned Entities with explicit key
If an explicit key to an owned entity then they are mapped as a normal entity and the navigation property to the owned entity adds `[Composition]`

This makes the owned entity available during Insert, Update and Delete operations and prevents if from accidentaly having all fields set to `null`.

## Complex Types

The *Complex Types* introduced in EF Core 8 are *partially supported** with some limitations.

1. The types are mapped to to OpenRiaServices's [Complex Types]
2. Any ef core configuration/metadata applied to the ComplexType (as part of fluent configuration) **IS NOT** discovered.
The `DbDomainServiceDescriptionProvider` and `DbDomainService` classes does not have any special handling of *Complex Types*.
* Attributes on the types are discovered as expected using the normal built in reflection based attribute discovery
Loading

0 comments on commit 34d62dd

Please sign in to comment.