EF Core создает таблицу из абстрактного класса - PullRequest
0 голосов
/ 01 ноября 2018

Работая над Asp.Net Core Web Api, я пытался сделать мою модель домена максимально сухой. Поэтому сначала я пошел по по этой ссылке , чтобы создать базовый объект со всеми полями, которые я знал, что буду нужно в моем приложении (я в значительной степени вставил код, поэтому я не собираюсь вставлять его сюда снова). Поработав немного больше, я хотел добавить File Uploading в мой проект. Чтобы понять, что я создал два класса с именами 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, потому что хотел избежать повторения с теми же полями и методами. Класс File наследуется от Entity (из статьи выше) и имеет общие поля, такие как FileName и Filesize:

 /// <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
    }

И здесь начинается моя дилемма: Когда я пытаюсь применить Code First Migration, я получаю следующее в Migration:

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

Этого не должно быть, поскольку я хочу, чтобы мои производные классы были только таблицами, а не базовым классом. Я уже пытался разрешить это с помощью Ignore on Modelbuilder в моем OnModelCreating:

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 отсутствует в БД (я пытаюсь добавить его с помощью этой миграции), а таблица файлов, которая должна быть создана, в точности соответствует всем полям класса 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 голосов
/ 01 ноября 2018

Ну, хорошо, хорошо,

теперь, после того, как накажу себя за то, что недостаточно читаю мои предыдущие миграции, я снова отвечаю на мой вопрос:

Я попытался добавить следующую строку непосредственно в 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);
            }

Предоставлено данным Вопросом по SO

И это теперь касается всех конфигураций, то есть я получаю правильное имя и количество полей для всех таблиц.

Спасибо, Иван Стоев, за попытку разобраться. Ваш комментарий дал мне правильную подсказку.

...