odata / modelbuilder Goto Github PK
View Code? Open in Web Editor NEWA project to generate Edm (Entity Data Model) from CLR types
A project to generate Edm (Entity Data Model) from CLR types
See detail at:
OData/WebApi#888
TopSupportedConfiguration.ToEdmExpression
creates a EdmRecordExpression
, but TopSupported
is a Core.Tag.
OData ModelBuilder
Students
new ODataConventionModelBuilder().EntitySet<Student>("Students").HasTopSupported().IsTopSupported(false);
$metadata
fileThe Students entity set has an annotation <Annotation Term="Org.OData.Capabilities.V1.TopSupported" Bool="true" />
The Students entity set has an annotation
<Annotation Term="Org.OData.Capabilities.V1.TopSupported">
<Record>
<PropertyValue Property="TopSupported" Bool="false"/>
</Record>
</Annotation>
See detail at: OData/WebApi#2297
Term is used to create the vocabulary annotation.
Please consider to support term configuration.
Wanting to do something very similar to this but running into the same errors.
Is there a way to do this with the current Microsoft.AspNetCore.OData 8.02?
In an Upgrade attempt from Odata 7.x to 8, we've encountered the Exception: System.NotSupportedException: Rebinding is not supported, without any clue which entitytype/entityset/navigationproperty.
Could you include the entity type in the exception so we can debug our model in a more pratical manner?
Code which I am refering to is:
So far, all the convention rules are hard coded in the convention model builder.
We'd consider to create a convention provider.
Developers often use OData and Entity Framework Core (EFCore) together to build services. It is frequently the case that the same data models are in shared C# code. In fact, many Microsoft OData tutorials use OData and EFCore together with shared data models. With the release of C# 8, the concept of NonNullable Reference Types (NRT) was introduced as a powerful way to strongly control nullability and greatly reduce the perennial bugs caused by dereferencing a null reference. With the release of EFCore 5, its data modeler will correctly handle NRTs.
The OData.Modelbuilder does not recognize NRTs which will cause a shared data model to have inconsistent handling of Nullability.
OData ModelBuilder 1.0.6
#nullable enable
public record Country
{
[Key]
[DisplayName("ISO Code")]
[MaxLength(2)]
[MinLength(2)]
public string ISOCode { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
}
When building the EFCore and SQL database the property Name is created NOT NULL:
CREATE TABLE [dbo].[Country] (
[ISOCode] NVARCHAR (2) NOT NULL,
[Name] NVARCHAR (MAX) NOT NULL,
CONSTRAINT [PK_Country] PRIMARY KEY CLUSTERED ([ISOCode] ASC));
However, when built with the OData endpoint there is no NULLABLE=“false” facet:
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="DSS.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Country">
<Key>
<PropertyRef Name="ISOCode" />
</Key>
<Property Name="ISOCode" Type="Edm.String" Nullable="false" MaxLength="2" />
<Property Name="Name" Type="Edm.String" />
</EntityType>
</Schema>
<Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityContainer Name="Container">
<EntitySet Name="Countries" EntityType="DSS.Models.Country" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
<Property Name="Name" Type="Edm.String" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
One straightforward workaround is to simply add a [Required]
attribute to the Name
property or to configure it explicitly using the model builder fluent API.
A better solution is to upgrade the OData model builder to be functionally equivalent to EFCore.
The design is heavily influenced by the implementation in EFCore, however EFCore code is structured to repeatedly ask if a type IsNullable()
. In contrast, the OData ModelBuilder has a powerful set of conventions that are called based on the CustomAttributes in the CLR Type. This allows the OData ModelBuilder to efficiently determine an entity’s nullability as it traverses the entity sets.
It is important to note that NRTs are primarily a compiler tool for static analysis of code and generates the exact same MSIL regardless of the nullability context. But the compiler does generate two special internal only attributes NullableAttribute
and NullableContextAttribute
and decorates types appropriately. A complete design of the Nullable Metadata can be found here.
NullableAttributeEdmPropertyConvention()
The NullableAttribute
may appear on any CLR type that is defined within a nullable context. By creating a new IODataModelConvention
every property can be processed and its Nullability can be set as appropriate.
For a property, the logic is straight forward:
RequiredAttribute
, with a higher precedence that are called earlier.NullableFlags
field is not set to 1 (nonnullable), do nothing.Get
method has the return MaybeNullAttribute
set, do nothing.NullableContextAttributeEdmTypeConvention()
The NullableContextAttribute
is an optimization used to indicate the nullability of type references that have no NullableAttribute
annotations. It may be placed type or method declarations. The compiler has an algorithm to determine the value of the context flag, a simplistic summary is it uses the most common value in the scope.
With the OData type conventions processing, this attribute can be easily discovered on a type and applied efficiently down to its declared properties. By contrast, EFCore when processing any property without a NullableAttribute
annotation must look up to the declaring type to determine if it has a NullableContextAttribute
annotation that applies. This results (without a cache) in the Reflection of a type proportional to the number of declared properties. Thus, unlike EFCore, a type cache in OData is not needed.
For a type, the logic is as follows:
NullableContextAttribute
flag is not 1, do nothing. Because the property default is Nullable.NullableContextAttribute
is not inherited.NullableAttribute
annotation, do nothing.MaybeNullAttribute
set, do nothing.This algorithm does not handle all possible complicated types, but matches the algorithm used by EFCore. Notable gaps include:
NullableAttribute
annotation for a complicated type (e.g., array) will have a set of flags representing elements or containing type. They are not supported due to the complexity of the nullability semantics, their relative rarity, and possibility they cannot be modeled in the CSDL.For code reuse and cleanliness, the following extensions to PropertyConfiguration
are created:
bool NonNullable()
Returns true if the property has been set to NonNullable.
bool ReturnMaybeNull()
Returns true if the property has the MaybeNull annotation.
void SetNonNullable()
Sets the property to be NonNullable.
https://github.com/dotnet/roslyn/blob/master/docs/features/nullable-metadata.md
Microsoft.OData.ModelBuilder.PrimitivePropertyConfigurationExtensions.AsDate errors on new net6 type DateOnly.
i.e.
"The property 'DateField' on type 'Northwind.Entities.AllType' must be a System.DateTime property. (Parameter 'property')"
OData ModelBuilder 1.0.9 and older
Have an entity that uses DataOnly as a property type i.e.:
[Column("dateField", TypeName = "date")]
public DateOnly? DateField { get; set; }
a DateOnly property would translate to Edm.Date in ModelBuilder
"The property 'DateField' on type 'Northwind.Entities.AllType' must be a System.DateTime property. (Parameter 'property')"
Short summary (3-5 sentences) describing the issue.
Which assemblies and versions are known to be affected e.g. OData ModelBuilder 1.x
There's a static instance for the default assembly resolver.
https://github.com/OData/ModelBuilder/blob/master/src/Microsoft.OData.ModelBuilder/Helpers/DefaultAssemblyResolver.cs#L21
In multiple projects scenario, for example there's "v1" and "v2" model builder.
"v1" reference Assembly A, B, C.
"v2" reference Assembly A, B, C, and D.
If we build the V1 first, Default assembly only loads "A, B, C".
When build "v2", since the default assembly statically in memory, it load "A, B, C", then there's no opportunity to load "D".
What would happen if there wasn't a bug.
What is actually happening.
Work around, create your own AssemblyResolver and input into the convention model builder.
From: David Robinson [email protected]
Sam,
As I have been hacking away at the impact of NonNullable Reference types in the OData Convention builder (I am close to making it work), I think I have found a bug in ReconfigureEntityTypesAsComplexType().
The situation I am seeing is that as GetEdmModel is doing its work, it builds up all of the types and code of this form:
public class Employee
{
// Other properties elided
public Address HomeAddress { get; set; } = new Address();
}
public class Address
{
// Other properties elided
public string State { get; set; } = string.Empty;
public string IgnoreThis { get; set; } = "IgnoreThis";
}
It will initially create through MapTypes the HomeAddress as a NavigationPropertyConfiguration and set its Multiplicity. After the initial type mapping it then calls RediscoverComplexTypes to detect the Navigation types that are misconfigured (e.g. Address has no Key) and need to really be Complex type.
The code then flows into ReconfigureEntityTypesAsComplexType until it gets to this comment:
// go through all structural types to remove all properties defined by this mis-configed type.
Where it iterates over the structural types that need to be “patched”, in this case Employee which has a Navigation type property HomeAddress of type Address.
It then checks on the Multiplicity to determine what it should do.
It first checks if EdmMultiplicity.Many and handles that as a Collection by calling AddCollectionProperty.
Else, it treats all other values of Multiplicity as a Complex property.
This is where the bug occurs. When calling AddComplexProperty it will set the Nullability property based on the underlying ClrType, which is correct for EdmMultiplicity.ZeroOrOne but is not correct for EdmMultiplicity.One as that means it cannot be zero, thus Nullability must == false and the code is setting it to be true. A bug!
Looking at the code that I changed to address this (restyling into a switch instead of a long if-then-else chain):
switch (propertyToBeRemoved.Multiplicity)
{
case EdmMultiplicity.Many:
propertyConfiguration =
structuralToBePatched.AddCollectionProperty(propertyToBeRemoved.PropertyInfo);
break;
case EdmMultiplicity.One:
propertyConfiguration =
structuralToBePatched.AddComplexProperty(propertyToBeRemoved.PropertyInfo);
(propertyConfiguration as ComplexPropertyConfiguration).NullableProperty = false;
break;
case EdmMultiplicity.ZeroOrOne:
case EdmMultiplicity.Unknown:
default:
propertyConfiguration =
structuralToBePatched.AddComplexProperty(propertyToBeRemoved.PropertyInfo);
break;
}
I found this bug in my code where I have a test case where HomeAddress is set to be a NonNullable type, but I it can also be reproduced in the shipping version by simply adding the Required Attribute to the HomeAddress property. I will see if I can create a reproducable test case in the existing test repository.
-David
[Capabilities vocabulary support(https://learn.microsoft.com/en-us/odata/webapi/capabilities-vocabulary-support) is working well for an entity until we use Key value binding
public class MyEntity
{
public Guid Id { get; set; }
[Required]
[NotSortable]
[NotFilterable]
public required string SomeStringKey { get; set; }
}
private IEdmModel BuildModel()
{
var builder = new ODataConventionModelBuilder();
var entitySetConfiguration = builder.EntitySet<MyEntity>(nameof(MyEntity));
// If this line is present FilterRestrictions and SortRestrictions are removed from metadata.xml
entitySetConfiguration.EntityType.HasKey(e => new { e.SomeStringKey, e.Id });
return builder.GetEdmModel();
}
Capabilities annotations should be declared whatever the entity key definition
Capabilities annotations are ignored if the entity declare a composite
This may be related to #10, but I'm not sure if #10 is just obsolete or not
The ODataConventionalModelBuilder throws an exception durng registration of entities with a key property that is of a nullable enum type:
System.ArgumentException: Type provided must be an Enum. (Parameter 'enumType')
Although key properties are never nullable in the created model, it should be possible to register key properties of a nullable enum type since there is no error for key properties of a nullable primitve type like int?
.
OData ModelBuilder 1.0.9
Example code:
using Microsoft.OData.ModelBuilder;
using System.ComponentModel.DataAnnotations;
var builder = new ODataConventionModelBuilder();
builder.EnableLowerCamelCase();
builder.EntityType<Customer>();
var model = builder.GetEdmModel();
enum Classification
{
None,
Test
}
class Customer
{
[Key]
public Classification? Classification { get; set; }
}
The model builder would successfully create a model with a Customer
entity that has a key property of type Classification
.
Since it is a key property, it would be marked as non-nullable in the created model.
When creating the EdmModel the following exception occurs:
Unhandled exception. System.ArgumentException: Type provided must be an Enum. (Parameter 'enumType')
at System.RuntimeType.GetEnumUnderlyingType()
at System.Enum.GetUnderlyingType(Type enumType)
at Microsoft.OData.ModelBuilder.EnumTypeConfiguration..ctor(ODataModelBuilder builder, Type clrType)
at Microsoft.OData.ModelBuilder.ODataModelBuilder.AddEnumType(Type clrType)
at Microsoft.OData.ModelBuilder.ODataConventionModelBuilder.AddEnumType(Type type)
at Microsoft.OData.ModelBuilder.EntityTypeConfiguration.HasKey(PropertyInfo keyProperty)
at Microsoft.OData.ModelBuilder.Conventions.Attributes.KeyAttributeEdmPropertyConvention.Apply(StructuralPropertyConfiguration edmProperty, StructuralTypeConfiguration structuralTypeConfiguration, Attribute attribute, ODataConventionModelBuilder model)
at Microsoft.OData.ModelBuilder.Conventions.Attributes.AttributeEdmPropertyConvention`1.Apply(TPropertyConfiguration edmProperty, StructuralTypeConfiguration structuralTypeConfiguration, ODataConventionModelBuilder model)
at Microsoft.OData.ModelBuilder.Conventions.Attributes.AttributeEdmPropertyConvention`1.Apply(PropertyConfiguration edmProperty, StructuralTypeConfiguration structuralTypeConfiguration, ODataConventionModelBuilder model)
at Microsoft.OData.ModelBuilder.ODataConventionModelBuilder.ApplyPropertyConvention(IEdmPropertyConvention propertyConvention, StructuralTypeConfiguration edmTypeConfiguration)
at Microsoft.OData.ModelBuilder.ODataConventionModelBuilder.ApplyTypeAndPropertyConventions(StructuralTypeConfiguration edmTypeConfiguration)
at Microsoft.OData.ModelBuilder.ODataConventionModelBuilder.MapTypes()
at Microsoft.OData.ModelBuilder.ODataConventionModelBuilder.GetEdmModel()
at Program.<Main>$(String[] args) in C:\Temp\Example\Program.cs:line 8
HasKey
-method.int?
are successfull. The model contains a key property of the primitive type that is marked as non-nullable since it is a key.TypeHelper.GetUnderlyingType
for the case of an enum type in the HasKey
-method which is used in other code parts of the project.In .Net 7 annotations for basetype properties are only added to the last type.
Microsoft.OData.ModelBuilder 1.0.9
This program throws InvalidOperationException
, while it runs fine when TargetFramework is net6.0
.
ConsoleApp.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OData.ModelBuilder" Version="1.0.9" />
</ItemGroup>
</Project>
Program.cs
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
var builder = new ODataConventionModelBuilder();
builder.EnableLowerCamelCase();
builder.EntityType<Derived1>();
builder.EntityType<Derived2>();
var model = builder.GetEdmModel();
var entityType1 = model.FindType("Default.Derived1") as EdmEntityType;
var idProperty1 = entityType1!.FindProperty("id");
var annotation1 = model.GetAnnotationValue<ClrPropertyInfoAnnotation>(idProperty1);
var entityType2 = model.FindType("Default.Derived2") as EdmEntityType;
var idProperty2 = entityType2!.FindProperty("id");
var annotation2 = model.GetAnnotationValue<ClrPropertyInfoAnnotation>(idProperty2);
if (annotation1 is null || annotation2 is null)
{
throw new InvalidOperationException();
}
public abstract class BaseEntity
{
public Guid Id { get; set; }
}
public sealed class Derived1 : BaseEntity { }
public sealed class Derived2 : BaseEntity { }
Annotations for basetype properties should be added to both derived types.
Annotations for basetype properties are only added to the last type added.
Stepping through the code shows that EdmTypeMap.EdmProperties
has only 1 PropertyInfo
item while in .Net 6 there are 2.
I have found 2 workarounds, either:
builder.EntityType<BaseEntity>();
Short summary (3-5 sentences) describing the issue.
Which assemblies and versions are known to be affected e.g. OData ModelBuilder 1.x
What would happen if there wasn't a bug.
What is actually happening.
Optional, details of the root cause if known. Delete this section if you have no additional details to add.
When we have the private set for the Key property. The EF works well with it Read-only properties. And we have all BO with private/proteced Key set.
The next classes set :
public class TestUser {
public int ID { get; protected set; }
public string FullName { get; set; }
public virtual IList<TestRole> Roles { get; set; }
}
public class TestRole {
public int ID { get; protected set; }
public string FullName { get; set; }
public virtual IList<TestUser> Users { get; set; }
}
Will work properly when we register both types and their keys manually:
ODataConventionModelBuilder builder = ...
Type typeToRegister = typeof(TestUser);
builder.AddEntityType(typeToRegister ).HasKey(typeToRegister.GetProperty("Id"))
typeToRegister = typeof(TestRole);
builder.AddEntityType(typeToRegister ).HasKey(typeToRegister.GetProperty("Id"))
But it's not convenient to register all types from types hierarchy manually. In general we want to register only top hierarchy objects and provide registration of other types to the system.
And when we register only one type from that example:
Type typeToRegister = typeof(TestUser);
builder.AddEntityType(typeToRegister ).HasKey(typeToRegister.GetProperty("Id"))
The TestRole type is resolved as Complex type and not the Entity type, because it can not discover it's Key property.
The Key property with private/protected set is discovered and type is considered as the Entity type.
The Model builder skip all properties with private/protected set.
The Microsoft.OData.ModelBuilder.ConventionsHelpers.GetAllProperties method doesn't return property with private set. After that the Microsoft.OData.ModelBuilder.EntityKeyConvention.GetKeyProperty cannot discover key property.
Nowadays, it's hard for almost everyone to create the vocabulary annotation manually, especially the Capabilities vocabulary.
So, consider to provide the configuration for vocabulary annotation,
When installing Microsoft.AspNetCore.OData, the following error occurs:
Restoring packages for D:\Users\Shimmy\Source\Repos\MyProject\MyProject.Api\MyProject.Api.csproj...
NU1107: Version conflict detected for System.ComponentModel.Annotations. Install/reference System.ComponentModel.Annotations 5.0.0 directly to project MyProject.Api to resolve this issue.
MyProject.Api -> Microsoft.EntityFrameworkCore.Tools 5.0.0 -> Microsoft.EntityFrameworkCore.Design 5.0.0 -> Microsoft.EntityFrameworkCore.Relational 5.0.0 -> Microsoft.EntityFrameworkCore 5.0.0 -> System.ComponentModel.Annotations (>= 5.0.0)
MyProject.Api -> Microsoft.AspNetCore.OData 8.0.0-preview2 -> Microsoft.OData.ModelBuilder 1.0.4 -> System.ComponentModel.Annotations (>= 4.6.0 && < 5.0.0).
Package restore failed. Rolling back package changes for 'MyProject.Api'.
Time Elapsed: 00:00:00.2568489
========== Finished ==========
Microsoft.OData.ModelBuilder
Should not cause package conflicts
Causes NuGet package conflicts
The DescriptionConfiguration class returned by extension methods such as HasDescription<TEntity>(this NavigationSourceConfiguration<TEntity> navigationSource)
returns a record edm expression, while methods in the EDM library such as GetLongDescriptionAnnotation(this IEdmModel model, IEdmVocabularyAnnotatable target) expects it to be IEdmStringConstantExpression. The vocabulary at the OASIS-TC also specifies that the annotation should be a string.
OData ModelBuilder 1.0.9
Create a simple console project.
dotnet new console --use-program-main
dotnet add package Microsoft.OData.ModelBuilder --version 1.0.9
dotnet add package Microsoft.OData.Edm --version 7.9.4
Paste the following into Program.cs
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
namespace odata_core_vocab_fault;
record Book(string ISBN);
class Program
{
static void Main(string[] args)
{
var builder = new ODataConventionModelBuilder();
var books = builder.EntitySet<Book>("Books");
books
.EntityType
.HasKey(e => e.ISBN);
books
.HasDescription()
.HasDescription("A collection of books.");
var edmModel = builder.GetEdmModel();
var entityset = edmModel.FindDeclaredEntitySet("Books");
var description = edmModel.GetDescriptionAnnotation(entityset);
Console.WriteLine(description);
}
}
The console prints "A collection of books."
The console prints nothing. Inspecting the result while debugging shows description
as null.
Support Capabilities Vocabulary via Attributes and Convention Builder
Followup on #9
.Net 6 Preview 4 adds support for 2 new BCL structures: DateOnly
and TimeOnly
DateOnly
maps very close to Edm.Date
, the only exception is that DateOnly
range is only 0-9999 CE, while Edm.Date is -9999-9999.
TimeOnly
maps very close to Edm.TimeOfDay
, except that the fractionalSeconds
has different precision.
The Convention Builder should recognize these types and map them accordingly.
Note: EF Core 6 does not yet map these types to SQL Server, it is expected in EF Core 7?
Interface IEdmEntitySet include interface IEdmVocabularyAnnotatable, so I can set, for example, optimistic concurrency properties for top-level entity set.
BUT, interface IEdmContainedEntitySet not include IEdmVocabularyAnnotatable, so I can't set optimistic concurrency properties for non-top level entity set.
Probably I am missing something, but element from top-level EntitySet and it child from selecondlevel ContainedEntitySet both can have concurrency properties.
Can somebody explain why contained entity set so restricted that them cannot be annotated?
Short summary (3-5 sentences) describing the issue.
Currently the model builder codebase can allow one to build expressions for annotations. Allowing the code to cache the expressions could enable users to perform a check to see if the constructor has anything or is empty.
This will allow other odata dependent libraries to use model builder's current annotations configurations for their own processing including WebApiAuthorizations
One good way to do this would be to make sure when item are added the property constructors are modified accordingly and the expression is built already.
Which assemblies and versions are known to be affected e.g. OData ModelBuilder 1.x
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.