在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);
}
}
是不是我的配置没有被应用?
答案 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尝试解决这个问题。您的评论给了我正确的提示。