EF Core正在根据抽象类创建表

时间:2018-11-01 01:57:43

标签: ef-code-first entity-framework-core asp.net-core-webapi ef-migrations

在Asp.Net Core Web Api上工作时,我试图将域模型设置为DRY。因此,我首先遵循this link创建一个基本实体,其中包含我知道我需要的所有字段应用程序(我几乎粘贴了代码,所以我不会再将其粘贴到这里)。工作多一点后,我想将文件上载添加到我的项目中。意识到我创建了两个类,分别称为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的抽象类,因为我想避免重复使用相同的Fields和Methods.File类继承自Entity(来自上一篇文章),并具有FileName和Filesize这样的常见Fiels:

 /// <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")

这不应该发生,因为我只想将派生类作为表而不是基类。我已经尝试在OnModelCreating中使用“在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();
}

但是输出仍然相同。因此,我在问如何解决这个问题,所以我只能在数据库中有派生类,而不能有抽象类。

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

/// <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类不在Db中(我正在尝试通过此迁移添加它),并且应创建的文件表已使TextFile类的所有文件都被删除。它仅被命名为错误。我对TextFile类的配置如下所示:

 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);
        }
    }

是不是我的配置没有被应用?

1 个答案:

答案 0 :(得分:1)

很好,

现在由于对自己的迁移没有足够准确的理解而受到惩罚之后,我回到了我的问题上:

我尝试将以下行直接添加到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尝试解决这个问题。您的评论给了我正确的提示。