Работая над 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);
}
}
Может ли быть так, что моя Конфигурация просто не применяется?