EF Core 正在从抽象 class 创建 table

EF Core is creating table from abstract class

在 Asp.Net 核心 Web Api 上工作时,我试图让我的域模型像 possible.Therefore 一样干,我首先遵循 this link 创建一个基础实体我知道我的应用程序中需要的所有字段(我几乎粘贴了代码,所以我不打算再次粘贴它)。再工作一点之后,我想将文件上传添加到我的项目中。要意识到我创建了两个 Classes,分别称为 Photo 和 TextFile:

    public class TextFile : File
        {
            #region Members
            /// <summary>
            /// The ForeignKey to the User
            /// </summary>
            public Guid UserId { get; private set; }

            /// <summary>
            /// The NavigationProperty to the User that added this Photo.
            /// </summary>
            public virtual User User { get; set; }
            #endregion

            #region Constructors
            /// <summary>
            /// For Ef Core
            /// </summary>
            private TextFile()
            { }

            /// <summary>
            /// Creates a new Instance of a TextFile.
            /// </summary>
            /// <param name="userId">The Id of the User that created this TextFile</param>
            public TextFile(Guid userId)
            {
                UserId = userId;
            }
            #endregion
        }

        /// <summary>
        /// Represents a Photo that got Uploaded
        /// </summary>
        public class Photo : File
        {
            #region Members
            /// <summary>
            /// Determines where this Image gets shown.
            /// </summary>
            public ImageOption? Option { get; private set; }

            /// <summary>
            /// The ForeignKey to the User
            /// </summary>
            public Guid UserId { get; private set; }

            /// <summary>
            /// The NavigationProperty to the User that added this Photo.
            /// </summary>
            public virtual User User { get; set; }
            #endregion

            #region Constructors
            /// <summary>
            /// For EF Core
            /// </summary>
            private Photo()
            { }

            /// <summary>
            /// Basic Constructor
            /// </summary>
            /// <param name="userId"></param>
            public Photo(Guid userId)
            {
                UserId = userId;
            }
            #endregion

            #region Methods
            /// <summary>
            /// Sets the Image Option only once
            /// </summary>
            /// <param name="option"></param>
            public void SetImageOption(ImageOption option)
            {
                if (Option.HasValue)
                    return;
                else
                    Option = option;
            }      
            #endregion

        }

这里我创建了一个名为 File 的抽象 class 因为我想避免重复使用相同的字段和 Methods.The File class 继承自 Entity(来自上面的文章)和具有文件名和文件大小等常见字段:

 /// <summary>
    /// Base Class for all Files
    /// </summary>
    public abstract class File : Entity<Guid>
    {
        #region Members
        /// <summary>
        /// The name of the File
        /// </summary>
        public string FileName { get; private set; }

        /// <summary>
        /// The Path to the File
        /// </summary>
        public string FilePath { get; private set; }

        /// <summary>
        /// The Size of the File
        /// </summary>
        public int FileSize { get; private set; }

        public FileExtension Extension { get; private set; }
        #endregion

        #region Methods
        /// <summary>
        /// Creates a new Text File to be uploaded to the Database.
        /// </summary>
        /// <param name="file">The File to be Uploaded</param>
        /// <param name="relativeFolderPath">The Relative Path from the WebRoot.</param>
        /// <param name="userId">A UserId</param>
        /// <param name="extension">The Extension of this File.</param>
        /// <param name="token">A CancellationToken</param>
        /// <returns></returns>
        public static File CreateTextFile(IFormFile file, string relativeFolderPath, Guid userId, FileExtension extension, CancellationToken token)
        {
            token.ThrowIfCancellationRequested();
            var textFile = new TextFile(userId);
            textFile.SetFileProperties(file, relativeFolderPath, extension, token);

            return textFile;
        }

        /// <summary>
        /// Creates a new Image File Model
        /// </summary>
        /// <param name="file">The File to be uploaded</param>
        /// <param name="relativeFolderPath">The relative Path to the Folder this Image resides in.</param>
        /// <param name="userId">A UserId</param>
        /// <param name="extension">The File Extension</param>
        /// <param name="token">A CancellationToken</param>
        /// <returns></returns>
        public static File CreatePhoto(IFormFile file, string relativeFolderPath, Guid userId, FileExtension extension, CancellationToken token)
        {
            token.ThrowIfCancellationRequested();

            var photo = new Photo(userId);
            photo.SetFileProperties(file, relativeFolderPath, extension, token);

            return photo;
        }

        /// <summary>
        /// Set Properties on File Entity
        /// </summary>
        /// <param name="file">The File</param>
        /// <param name="relativeFolderPath">The Path extending from the WebRoot</param>
        /// <param name="extension">The File Extension</param>
        /// <param name="token">A CancellationToken</param>
        private void SetFileProperties(IFormFile file, string relativeFolderPath, FileExtension extension, CancellationToken token)
        {
            if(file == null)
                throw new ArgumentNullException(nameof(file));

            if(string.IsNullOrWhiteSpace(relativeFolderPath))
                throw new ArgumentNullException(nameof(relativeFolderPath));

            token.ThrowIfCancellationRequested();

            FileSize = (int) file.Length;

            Extension = extension;

            FileName = Guid.NewGuid() + "." + extension.ToString().ToLower();

            FilePath = Path.Combine(relativeFolderPath, FileName);
        }

        /// <summary>
        /// Sets the Extension of this File
        /// </summary>
        /// <param name="extension"></param>
        /// <param name="ext">The Extension of the File</param>
        /// <param name="token">A CancellationToken</param>
        private static void FindExtension(string extension, out FileExtension ext, CancellationToken token)
        {
            token.ThrowIfCancellationRequested();

            switch (extension.ToLower())
            {
                case ".jpg":
                    ext = FileExtension.Jpg;
                    break;

                case ".jpeg":
                    ext = FileExtension.Jpeg;
                    break;

                case ".png":
                    ext = FileExtension.Png;
                    break;

                case ".bmp":
                    ext = FileExtension.Bmp;
                    break;

                case ".gif":
                    ext = FileExtension.Gif;
                    break;

                case ".tif":
                    ext = FileExtension.Tif;
                    break;

                case ".tiff":
                    ext = FileExtension.Tiff;
                    break;

                case ".svg":
                    ext = FileExtension.Svg;
                    break;

                case ".doc":
                    ext = FileExtension.Doc;
                    break;

                case ".docx":
                    ext = FileExtension.Docx;
                    break;

                case ".odt":
                    ext = FileExtension.Odt;
                    break;

                case ".rtf":
                    ext = FileExtension.Rtf;
                    break;

                case ".txt":
                    ext = FileExtension.Txt;
                    break;

                case "xls":
                    ext = FileExtension.Xls;
                    break;

                case ".xlsx":
                    ext = FileExtension.Xlsx;
                    break;

                case ".ppt":
                    ext = FileExtension.Ppt;
                    break;

                case ".pptx":
                    ext = FileExtension.Pptx;
                    break;

                case ".pdf":
                    ext = FileExtension.Pdf;
                    break;

                default:
                    throw new InvalidFileExtensionException($"The Extension {extension.ToLower()} is not allowed.");
            }
        }

        /// <summary>
        /// Determines if the Specified Extension is a allowed Extension.
        /// Returns true in case the extension is a file extension.
        /// Returns False in case the Extension is a Image File.
        /// The FileExtension Parameter is always set
        /// </summary>
        /// <param name="extensionName">The extension as a string</param>
        /// <param name="extension">The Extension that this File has.</param>
        /// <param name="token">A CancellationToken</param>
        /// <returns></returns>
        public static bool IsTextFile(string extensionName, out FileExtension extension, CancellationToken token)
        {
            token.ThrowIfCancellationRequested();

            FindExtension(extensionName, out extension, token);

            return (int) extension > 8;
        }

        /// <summary>
        /// Determines if the Extension is a allowed Extension and a Image File.
        /// the Extension will always be set.
        /// </summary>
        /// <param name="extensionName">The Extension as string</param>
        /// <param name="extension">The FileExtension</param>
        /// <param name="token">A CancellationToken</param>
        /// <returns></returns>
        public static bool IsImageFile(string extensionName, out FileExtension extension, CancellationToken token)
        {
            token.ThrowIfCancellationRequested();

            FindExtension(extensionName, out extension, token);

            return (int)extension < 8;
        }
        #endregion
    }

这就是我的困境开始的地方:当我尝试应用代码优先迁移时,我在迁移中得到以下信息:

migrationBuilder.CreateTable(
                name: "Files",
                columns: table => new
                {
                    Id = table.Column<Guid>(nullable: false),
                    Created = table.Column<DateTime>(nullable: true),
                    LastModified = table.Column<DateTime>(nullable: true),
                    FileName = table.Column<string>(nullable: true),
                    FilePath = table.Column<string>(nullable: true),
                    FileSize = table.Column<int>(nullable: false),
                    Extension = table.Column<int>(nullable: false),
                    UserId = table.Column<Guid>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Files", x => x.Id);
                    table.ForeignKey(
                        name: "FK_Files_Users_UserId",
                        column: x => x.UserId,
                        principalTable: "Users",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "IX_Files_UserId",
                table: "Files",
                column: "UserId")

这不应该发生,因为我只想将派生的 classes 作为 Tables 而不是我的基础 Class。我已经尝试在我的 OnModelCreating 中使用 Ignore on Modelbuilder 解决它:

protected override void OnModelCreating(ModelBuilder builder)
{
    builder.ApplyConfiguration(new UserRoleConfiguration());

    builder.ApplyConfiguration(new UserTokenConfiguration());

    builder.Ignore<File>();

    builder.Entity<User>().OwnsOne(x => x.FullName, fullName =>
    {
        fullName.OwnsOne(x => x.FirstName, firstName =>
        {
            firstName.Property(p => p.FirstNamePart).HasColumnName("FirstName_FirstPart").HasMaxLength(255)
                .IsRequired();
            firstName.Property(p => p.NameSeperator).HasColumnName("FirstName_NameSeperator").HasMaxLength(5);
            firstName.Property(p => p.LastNamePart).HasColumnName("FirstName_LastPart").HasMaxLength(255);
        });
        fullName.OwnsOne(x => x.LastName, lastName =>
        {
            lastName.Property(p => p.FirstNamePart).HasColumnName("LastName_FirstPart").HasMaxLength(255)
                .IsRequired();
            lastName.Property(p => p.NameSeperator).HasColumnName("LastName_NameSeperator").HasMaxLength(5);
            lastName.Property(p => p.LastNamePart).HasColumnName("LastName_LastPart").HasMaxLength(255);
        });
    });

    builder.ApplyAllConfigurations();
}

但是输出还是一样。因此我想问如何解决这个问题,这样我就可以在数据库中只有派生的 classes 而不是抽象的 class.

我在此处发布您要求的代码:

/// <summary>
/// The User of this Application.
/// </summary>
public class User : Entity<Guid>
{
    /// <summary>
    /// Basic Constructor for the User
    /// </summary>
    public User()
    {
        UserRoles = new HashSet<UserRole>();
        UserClaims = new HashSet<UserClaim>();
        Tokens = new HashSet<UserToken>();
        Photos = new HashSet<Photo>();
        Files = new HashSet<TextFile>();
    }


    /// <summary>
    /// A Concurrency Stamp
    /// </summary>
    public string ConcurrencyStamp { get; set; }

    /// <summary>
    /// The Email of this User
    /// </summary>
    public string Email { get; set; }

    /// <summary>
    /// The Normalized Email of this User
    /// </summary>
    public string NormalizedEmail { get; set; }

    /// <summary>
    /// Flag that indicates if the User has Confirmed his Email.
    /// </summary>
    public bool EmailConfirmed { get; set; }

    /// <summary>
    /// The User Name of this User.
    /// </summary>
    public string Username { get; set; }

    /// <summary>
    /// The normalized User Name
    /// </summary>
    public string NormalizedUsername { get; set; }

    /// <summary>
    /// The hashed and salted Password.
    /// </summary>
    public string PasswordHash { get; set; }

    /// <summary>
    /// A Security Stamp to validate The Users Information
    /// </summary>
    public string SecurityStamp { get; set; }

    /// <summary>
    /// The Full Name of a User.
    /// </summary>
    public FullName FullName { get; set; }

    /// <summary>
    /// The specific Y-Number that identifies the User 
    /// </summary>
    public string YNumberId { get; set; }

    /// <summary>
    /// The YNumber of this User.
    /// </summary>
    public YNumber YNumber { get; set; }

    /// <summary>
    /// The Collection of Roles.
    /// </summary>
    public virtual ICollection<UserRole> UserRoles { get; }

    /// <summary>
    /// The Collection of User Claims.
    /// </summary>
    public virtual  ICollection<UserClaim> UserClaims { get; }

    public virtual ICollection<UserToken> Tokens { get; }

    public virtual ICollection<Photo> Photos { get; }

    public virtual ICollection<TextFile> Files { get; }
}

        /// <summary>
        /// Applies all Configurations in this Assembly to the specified ModelBuilder Instance.
        /// </summary>
        /// <param name="modelBuilder">The Instance of the ModelBuilder that configures the Database.</param>
        public static void ApplyAllConfigurations(this ModelBuilder modelBuilder)
        {
            var applyConfigurationMethodInfo = modelBuilder
                .GetType()
                .GetMethods(BindingFlags.Instance | BindingFlags.Public)
                .First(method => method
                    .Name
                    .Equals("ApplyConfiguration", StringComparison.OrdinalIgnoreCase));

            var ret = typeof(ApplicationDbContext)
                .Assembly
                .GetTypes()
                .Select(type =>
                    (type, i: type
                        .GetInterfaces()
                        .FirstOrDefault(i => i
                            .Name
                            .Equals(typeof(IEntityTypeConfiguration<>)
                                .Name, StringComparison.OrdinalIgnoreCase))))
                .Where(it => it.i != null)
                .Select(it => (et: it.i.GetGenericArguments()[0], configObject: Activator.CreateInstance(it.Item1)))
                .Select(it =>
                    applyConfigurationMethodInfo.MakeGenericMethod(it.et)
                        .Invoke(modelBuilder, new[] {it.configObject}));
        }

在检查我的代码时,我感觉错误不直接在迁移中,而是在应用我的配置的方法中。我在想,因为 TextFile Class 不在 Db 中(我试图通过此迁移添加它)并且应该创建的文件 Table 具有 TextFile [=41] 的所有字段=].只是名字错了。我对 TextFile class 的配置如下所示:

 public class TextFileConfiguration : IEntityTypeConfiguration<TextFile>
    {

        public void Configure(EntityTypeBuilder<TextFile> builder)
        {
            //Set Primary Key
            builder
                .HasKey(x => x.Id);

            //Add ValueGeneration
            builder
                .Property(x => x.Id)
                .UseSqlServerIdentityColumn();

            //Set Table Name
            builder
                .ToTable("TextFiles");

            //Make Filename Required with MaxLength of 50 (because filename = Guid + FileExtension)
            builder
                .Property(x => x.FileName)
                .IsRequired()
                .HasMaxLength(50);

            //Configure Inverse Navigation Property.
            builder
                .HasOne(x => x.User)
                .WithMany(y => y.Files)
                .HasForeignKey(z => z.UserId)
                .OnDelete(DeleteBehavior.Cascade);
        }
    }

会不会是我的配置没有得到应用?

很好很好,

在惩罚自己没有足够准确地阅读我以前的迁移之后,现在我回到回答我的问题:

我尝试将以下行直接添加到 OnModelCreating 中:

builder.Entity<TextFile>().ToTable("Text Files");

和魔法:

migrationBuilder.CreateTable(
                name: "Text Files",
                columns: table => new
                {
                    Id = table.Column<Guid>(nullable: false),
                    Created = table.Column<DateTime>(nullable: true),
                    LastModified = table.Column<DateTime>(nullable: true),
                    FileName = table.Column<string>(nullable: true),
                    FilePath = table.Column<string>(nullable: true),
                    FileSize = table.Column<int>(nullable: false),
                    Extension = table.Column<int>(nullable: false),
                    UserId = table.Column<Guid>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Files", x => x.Id);
                    table.ForeignKey(
                        name: "FK_Files_Users_UserId",
                        column: x => x.UserId,
                        principalTable: "Users",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "IX_Files_UserId",
                table: "Files",
                column: "UserId")

迁移是正确的。因此,我检查了我的 ApplyConfiguration 并意识到它根本没有应用任何配置。因此我将其更改为:

var implementedConfigTypes = Assembly.GetExecutingAssembly()
                .GetTypes()
                .Where(t => !t.IsAbstract
                            && !t.IsGenericTypeDefinition
                            && t.GetTypeInfo().ImplementedInterfaces.Any(i =>
                                i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>)));

            foreach (var configType in implementedConfigTypes)
            {
                dynamic config = Activator.CreateInstance(configType);
                modelBuilder.ApplyConfiguration(config);
            }

Courtesy of this Question on SO

这就是现在应用所有配置,这意味着我得到了所有表的正确名称和字段数量。

感谢 Ivan Stoev 试图解决这个问题。您的评论给了我正确的提示。