Skip to content

Commit

Permalink
Introduce KeylessAttribute
Browse files Browse the repository at this point in the history
Resolves #19246
Also related #19964

- Add KeylessAttribute in Abstractions packge
- Add convention to detect KeylessAttribute and configure the entityType to be keyless
- Scaffolding when using data annotations
  - Generate KeylessAttribute over entity class
  - Skip HasNoKey from fluent API configuration

Tests:
- Add tests for OwnedAttribute
- Add tests for KeylessAttribute
- Add test for scaffolding to verify behavior with data annotations
  • Loading branch information
smitpatel committed Feb 21, 2020
1 parent 4cbc5d7 commit 5c5d7c0
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 19 deletions.
15 changes: 15 additions & 0 deletions src/EFCore.Abstractions/KeylessAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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;

namespace Microsoft.EntityFrameworkCore
{
/// <summary>
/// Marks a type as keyless entity.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class KeylessAttribute : Attribute
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -457,9 +457,12 @@ private void GenerateKey(IKey key, IEntityType entityType, bool useDataAnnotatio
{
if (key == null)
{
var line = new List<string> { $".{nameof(EntityTypeBuilder.HasNoKey)}()" };
if (!useDataAnnotations)
{
var line = new List<string> { $".{nameof(EntityTypeBuilder.HasNoKey)}()" };

AppendMultiLineFluentApi(entityType, line);
AppendMultiLineFluentApi(entityType, line);
}

return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public virtual string WriteCode(IEntityType entityType, string @namespace, bool
{
_sb.AppendLine("using System.ComponentModel.DataAnnotations;");
_sb.AppendLine("using System.ComponentModel.DataAnnotations.Schema;");
_sb.AppendLine("using Microsoft.EntityFrameworkCore;"); // For attributes coming out of Abstractions
}

foreach (var ns in entityType.GetProperties()
Expand Down Expand Up @@ -132,9 +133,18 @@ protected virtual void GenerateEntityTypeDataAnnotations(
{
Check.NotNull(entityType, nameof(entityType));

GenerateKeylessAttribute(entityType);
GenerateTableAttribute(entityType);
}

private void GenerateKeylessAttribute(IEntityType entityType)
{
if (entityType.FindPrimaryKey() == null)
{
_sb.AppendLine(new AttributeWriter(nameof(KeylessAttribute)));
}
}

private void GenerateTableAttribute(IEntityType entityType)
{
var tableName = entityType.GetTableName();
Expand All @@ -156,7 +166,7 @@ private void GenerateTableAttribute(IEntityType entityType)
tableAttribute.AddParameter($"{nameof(TableAttribute.Schema)} = {_code.Literal(schema)}");
}

_sb.AppendLine(tableAttribute.ToString());
_sb.AppendLine(tableAttribute);
}
}

Expand Down Expand Up @@ -278,7 +288,7 @@ private void GenerateMaxLengthAttribute(IProperty property)

lengthAttribute.AddParameter(_code.Literal(maxLength.Value));

_sb.AppendLine(lengthAttribute.ToString());
_sb.AppendLine(lengthAttribute);
}
}

Expand All @@ -288,7 +298,7 @@ private void GenerateRequiredAttribute(IProperty property)
&& property.ClrType.IsNullableType()
&& !property.IsPrimaryKey())
{
_sb.AppendLine(new AttributeWriter(nameof(RequiredAttribute)).ToString());
_sb.AppendLine(new AttributeWriter(nameof(RequiredAttribute)));
}
}

Expand Down Expand Up @@ -351,7 +361,7 @@ private void GenerateForeignKeyAttribute(INavigation navigation)
foreignKeyAttribute.AddParameter($"nameof({navigation.ForeignKey.Properties.First().Name})");
}

_sb.AppendLine(foreignKeyAttribute.ToString());
_sb.AppendLine(foreignKeyAttribute);
}
}
}
Expand All @@ -372,7 +382,7 @@ private void GenerateInversePropertyAttribute(INavigation navigation)
? $"nameof({inverseNavigation.DeclaringEntityType.Name}.{inverseNavigation.Name})"
: _code.Literal(inverseNavigation.Name));

_sb.AppendLine(inversePropertyAttribute.ToString());
_sb.AppendLine(inversePropertyAttribute);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public virtual ConventionSet CreateConventionSet()

conventionSet.EntityTypeAddedConventions.Add(new NotMappedEntityTypeAttributeConvention(Dependencies));
conventionSet.EntityTypeAddedConventions.Add(new OwnedEntityTypeAttributeConvention(Dependencies));
conventionSet.EntityTypeAddedConventions.Add(new KeylessEntityTypeAttributeConvention(Dependencies));
conventionSet.EntityTypeAddedConventions.Add(new NotMappedMemberAttributeConvention(Dependencies));
conventionSet.EntityTypeAddedConventions.Add(new BaseTypeDiscoveryConvention(Dependencies));
conventionSet.EntityTypeAddedConventions.Add(propertyDiscoveryConvention);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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.ComponentModel.DataAnnotations.Schema;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
{
/// <summary>
/// A convention that ignores entity types that have the <see cref="KeylessAttribute" />.
/// </summary>
public class KeylessEntityTypeAttributeConvention : EntityTypeAttributeConventionBase<KeylessAttribute>
{
/// <summary>
/// Creates a new instance of <see cref="KeylessEntityTypeAttributeConvention" />.
/// </summary>
/// <param name="dependencies"> Parameter object containing dependencies for this convention. </param>
public KeylessEntityTypeAttributeConvention([NotNull] ProviderConventionSetBuilderDependencies dependencies)
: base(dependencies)
{
}

/// <summary>
/// Called after an entity type is added to the model if it has an attribute.
/// </summary>
/// <param name="entityTypeBuilder"> The builder for the entity type. </param>
/// <param name="attribute"> The attribute. </param>
/// <param name="context"> Additional information associated with convention execution. </param>
protected override void ProcessEntityTypeAdded(
IConventionEntityTypeBuilder entityTypeBuilder,
KeylessAttribute attribute,
IConventionContext<IConventionEntityTypeBuilder> context)
{
entityTypeBuilder.HasNoKey(fromDataAnnotation: true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public void Navigation_properties()
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace TestNamespace
{
Expand All @@ -58,7 +59,7 @@ public Post()
}
}
",
postFile.Code);
postFile.Code, ignoreLineEndingDifferences: true);
},
model =>
{
Expand Down Expand Up @@ -97,6 +98,7 @@ public void Navigation_property_with_same_type_and_navigation_name()
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace TestNamespace
{
Expand All @@ -112,7 +114,7 @@ public partial class Post
}
}
",
postFile.Code);
postFile.Code, ignoreLineEndingDifferences: true);
},
model =>
{
Expand Down Expand Up @@ -152,6 +154,7 @@ public void Navigation_property_with_same_type_and_property_name()
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace TestNamespace
{
Expand All @@ -167,7 +170,7 @@ public partial class Post
}
}
",
postFile.Code);
postFile.Code, ignoreLineEndingDifferences: true);
},
model =>
{
Expand Down Expand Up @@ -208,6 +211,7 @@ public void Navigation_property_with_same_type_and_other_navigation_name()
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace TestNamespace
{
Expand All @@ -227,7 +231,7 @@ public partial class Post
}
}
",
postFile.Code);
postFile.Code, ignoreLineEndingDifferences: true);
},
model =>
{
Expand Down Expand Up @@ -275,6 +279,7 @@ public void Composite_key()
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace TestNamespace
{
Expand All @@ -287,7 +292,7 @@ public partial class Post
}
}
",
postFile.Code);
postFile.Code, ignoreLineEndingDifferences: true);
},
model =>
{
Expand All @@ -302,15 +307,105 @@ public void Views_dont_generate_TableAttribute()
Test(
modelBuilder => modelBuilder.Entity("Vista").ToView("Vistas", "dbo"),
new ModelCodeGenerationOptions { UseDataAnnotations = true },
code => Assert.DoesNotContain(
"[Table(",
code.AdditionalFiles.First(f => f.Path == "Vista.cs").Code),
code =>
{
var vistaFile = code.AdditionalFiles.First(f => f.Path == "Vista.cs");
Assert.Equal(
@"using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace TestNamespace
{
[Keyless]
public partial class Vista
{
}
}
",
vistaFile.Code, ignoreLineEndingDifferences: true);
},
model =>
{
var entityType = model.FindEntityType("TestNamespace.Vista");
Assert.Equal("Vistas", entityType.GetTableName());
Assert.Equal("dbo", entityType.GetSchema());
});
}

[ConditionalFact]
public void Keyless_entity_generates_KeylesssAttribute()
{
Test(
modelBuilder => modelBuilder.Entity("Vista").HasNoKey(),
new ModelCodeGenerationOptions { UseDataAnnotations = true },
code =>
{
var vistaFile = code.AdditionalFiles.First(f => f.Path == "Vista.cs");
Assert.Equal(
@"using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace TestNamespace
{
[Keyless]
public partial class Vista
{
}
}
",
vistaFile.Code, ignoreLineEndingDifferences: true);
Assert.Equal(
@"using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace TestNamespace
{
public partial class TestDbContext : DbContext
{
public TestDbContext()
{
}
public TestDbContext(DbContextOptions<TestDbContext> options)
: base(options)
{
}
public virtual DbSet<Vista> Vista { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.
optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase"");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}
",
code.ContextFile.Code, ignoreLineEndingDifferences: true);
},
model =>
{
var entityType = model.FindEntityType("TestNamespace.Vista");
Assert.Null(entityType.FindPrimaryKey());
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ protected void Test(
{
References =
{
BuildReference.ByName("Microsoft.EntityFrameworkCore.Abstractions"),
BuildReference.ByName("Microsoft.EntityFrameworkCore"),
BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational"),
BuildReference.ByName("Microsoft.EntityFrameworkCore.SqlServer")
Expand Down
Loading

0 comments on commit 5c5d7c0

Please sign in to comment.