Skip to content

Commit

Permalink
[WIP] Basic support for non-nullable references
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Mar 11, 2019
1 parent 1282c16 commit abc1c4c
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,15 @@ var servicePropertyDiscoveryConvention
var backingFieldConvention = new BackingFieldConvention(logger);
var concurrencyCheckAttributeConvention = new ConcurrencyCheckAttributeConvention(logger);
var databaseGeneratedAttributeConvention = new DatabaseGeneratedAttributeConvention(logger);
var requiredPropertyAttributeConvention = new RequiredPropertyAttributeConvention(logger);
var requiredPropertyConvention = new RequiredPropertyConvention(logger);
var maxLengthAttributeConvention = new MaxLengthAttributeConvention(logger);
var stringLengthAttributeConvention = new StringLengthAttributeConvention(logger);
var timestampAttributeConvention = new TimestampAttributeConvention(logger);

conventionSet.PropertyAddedConventions.Add(backingFieldConvention);
conventionSet.PropertyAddedConventions.Add(concurrencyCheckAttributeConvention);
conventionSet.PropertyAddedConventions.Add(databaseGeneratedAttributeConvention);
conventionSet.PropertyAddedConventions.Add(requiredPropertyAttributeConvention);
conventionSet.PropertyAddedConventions.Add(requiredPropertyConvention);
conventionSet.PropertyAddedConventions.Add(maxLengthAttributeConvention);
conventionSet.PropertyAddedConventions.Add(stringLengthAttributeConvention);
conventionSet.PropertyAddedConventions.Add(timestampAttributeConvention);
Expand Down Expand Up @@ -172,7 +172,7 @@ var servicePropertyDiscoveryConvention
conventionSet.ModelBuiltConventions.Add(new CacheCleanupConvention(logger));

conventionSet.NavigationAddedConventions.Add(backingFieldConvention);
conventionSet.NavigationAddedConventions.Add(new RequiredNavigationAttributeConvention(logger));
conventionSet.NavigationAddedConventions.Add(new RequiredNavigationConvention(logger));
conventionSet.NavigationAddedConventions.Add(inversePropertyAttributeConvention);
conventionSet.NavigationAddedConventions.Add(foreignKeyPropertyDiscoveryConvention);
conventionSet.NavigationAddedConventions.Add(relationshipDiscoveryConvention);
Expand All @@ -194,7 +194,7 @@ var servicePropertyDiscoveryConvention
conventionSet.PropertyFieldChangedConventions.Add(keyAttributeConvention);
conventionSet.PropertyFieldChangedConventions.Add(concurrencyCheckAttributeConvention);
conventionSet.PropertyFieldChangedConventions.Add(databaseGeneratedAttributeConvention);
conventionSet.PropertyFieldChangedConventions.Add(requiredPropertyAttributeConvention);
conventionSet.PropertyFieldChangedConventions.Add(requiredPropertyConvention);
conventionSet.PropertyFieldChangedConventions.Add(maxLengthAttributeConvention);
conventionSet.PropertyFieldChangedConventions.Add(stringLengthAttributeConvention);
conventionSet.PropertyFieldChangedConventions.Add(timestampAttributeConvention);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class RequiredNavigationConvention : INavigationAddedConvention
{
private const string NullableAttributeFullName = "System.Runtime.CompilerServices.NullableAttribute";
private Type _nullableAttrType;
private FieldInfo _nullableFlagsFieldInfo;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public RequiredNavigationConvention([NotNull] IDiagnosticsLogger<DbLoggerCategory.Model> logger)
{
Logger = logger;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
protected virtual IDiagnosticsLogger<DbLoggerCategory.Model> Logger { get; }

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual InternalRelationshipBuilder Apply(InternalRelationshipBuilder relationshipBuilder, Navigation navigation)
{
Check.NotNull(relationshipBuilder, nameof(relationshipBuilder));
Check.NotNull(navigation, nameof(navigation));

var entityType = navigation.DeclaringEntityType;
if (!entityType.HasClrType())
{
return relationshipBuilder;
}

var property = entityType.GetRuntimeProperties().Find(navigation.Name);
if (property == null || !IsRequired(property, out var withRequiredAttr))
{
return relationshipBuilder;
}

if (!navigation.IsDependentToPrincipal())
{
var inverse = navigation.FindInverse();
if (inverse != null)
{
if (IsRequired(inverse.PropertyInfo, out _))
{
Logger.RequiredAttributeOnBothNavigations(navigation, inverse);
return relationshipBuilder;
}
}

if (!navigation.ForeignKey.IsUnique
|| relationshipBuilder.Metadata.GetPrincipalEndConfigurationSource() != null)
{
return relationshipBuilder;
}

var newRelationshipBuilder = relationshipBuilder.RelatedEntityTypes(
relationshipBuilder.Metadata.DeclaringEntityType,
relationshipBuilder.Metadata.PrincipalEntityType,
ConfigurationSource.Convention);

if (newRelationshipBuilder == null)
{
return relationshipBuilder;
}

Logger.RequiredAttributeOnDependent(newRelationshipBuilder.Metadata.DependentToPrincipal);
relationshipBuilder = newRelationshipBuilder;
}

return relationshipBuilder.IsRequired(true, withRequiredAttr ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention)
?? relationshipBuilder;
}

bool IsRequired(PropertyInfo property, out bool withRequiredAttr)
{
if (Attribute.IsDefined(property, typeof(RequiredAttribute)))
{
withRequiredAttr = true;
return true;
}

if (Attribute.GetCustomAttributes(property) is Attribute[] attributes &&
attributes.FirstOrDefault(a => a.GetType().FullName == NullableAttributeFullName) is Attribute attribute)
{
// In theory there may be multiple versions of [Nullable] in the same model. Cache the attribute's FieldInfo.
if (attribute.GetType() != _nullableAttrType)
{
_nullableFlagsFieldInfo = attribute.GetType().GetField("NullableFlags");
_nullableAttrType = attribute.GetType();
}

// For the interpretation of NullableFlags, see
// https://github.com/dotnet/roslyn/blob/master/docs/features/nullable-reference-types.md#annotations
if (_nullableFlagsFieldInfo?.GetValue(attribute) is byte[] flags && flags.Length >= 0 && flags[0] == 1)
{
withRequiredAttr = false;
return true;
}
}

withRequiredAttr = false;
return false;
}
}
}

This file was deleted.

100 changes: 100 additions & 0 deletions src/EFCore/Metadata/Conventions/Internal/RequiredPropertyConvention.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class RequiredPropertyConvention : IPropertyAddedConvention, IPropertyFieldChangedConvention
{
private const string NullableAttributeFullName = "System.Runtime.CompilerServices.NullableAttribute";
private Type _nullableAttrType;
private FieldInfo _nullableFlagsFieldInfo;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
protected virtual IDiagnosticsLogger<DbLoggerCategory.Model> Logger { get; }

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public RequiredPropertyConvention([NotNull] IDiagnosticsLogger<DbLoggerCategory.Model> logger)
{
Logger = logger;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual InternalPropertyBuilder Apply(InternalPropertyBuilder propertyBuilder)
{
Check.NotNull(propertyBuilder, nameof(propertyBuilder));

var memberInfo = propertyBuilder.Metadata.GetIdentifyingMemberInfo();
if (memberInfo == null || !IsRequired(memberInfo, out var withRequiredAttr))
{
return propertyBuilder;
}

propertyBuilder.IsRequired(true, withRequiredAttr ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);

return propertyBuilder;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual bool Apply(InternalPropertyBuilder propertyBuilder, FieldInfo oldFieldInfo)
{
Apply(propertyBuilder);
return true;
}

bool IsRequired(MemberInfo member, out bool withRequiredAttr)
{
if (Attribute.IsDefined(member, typeof(RequiredAttribute)))
{
withRequiredAttr = true;
return true;
}

if (Attribute.GetCustomAttributes(member) is Attribute[] attributes &&
attributes.FirstOrDefault(a => a.GetType().FullName == NullableAttributeFullName) is Attribute attribute)
{
// In theory there may be multiple versions of [Nullable] in the same model. Cache the attribute's FieldInfo.
if (attribute.GetType() != _nullableAttrType)
{
_nullableFlagsFieldInfo = attribute.GetType().GetField("NullableFlags");
_nullableAttrType = attribute.GetType();
}

// For the interpretation of NullableFlags, see
// https://github.com/dotnet/roslyn/blob/master/docs/features/nullable-reference-types.md#annotations
if (_nullableFlagsFieldInfo?.GetValue(attribute) is byte[] flags && flags.Length >= 0 && flags[0] == 1)
{
withRequiredAttr = false;
return true;
}
}

withRequiredAttr = false;
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -820,8 +820,8 @@ public void Navigation_attribute_convention_runs_for_private_property()
Assert.False(referenceBuilder.Metadata.Properties.First().IsNullable);
}

private RequiredNavigationAttributeConvention CreateRequiredNavigationAttributeConvention()
=> new RequiredNavigationAttributeConvention(CreateLogger());
private RequiredNavigationConvention CreateRequiredNavigationAttributeConvention()
=> new RequiredNavigationConvention(CreateLogger());

public ListLoggerFactory ListLoggerFactory { get; }
= new ListLoggerFactory(l => l == DbLoggerCategory.Model.Name);
Expand Down
Loading

0 comments on commit abc1c4c

Please sign in to comment.