Of course , could be , Here are our configurations :
**OdmsDbContextModelCreatingExtensions inside: **
/* Configure all entities here. */
builder.Entity<Model>(b =>
{
b.ToTable(OdmsDbProperties.DbTablePrefix + "Models", OdmsDbProperties.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.SchemaName).HasMaxLength(ModelConsts.MaxSchemaNameLength).HasColumnName(nameof(Model.SchemaName)).IsRequired();
b.Property(x => x.ServerName).HasMaxLength(ModelConsts.MaxServerNameLength).HasColumnName(nameof(Model.ServerName)).IsRequired();
b.Property(x => x.DatabaseType).HasMaxLength(ModelConsts.MaxDatabaseTypeLength).HasColumnName(nameof(Model.DatabaseType));
b.Property(x => x.Password).HasMaxLength(ModelConsts.MaxEncryptedPasswordLength).HasColumnName(nameof(Model.Password));
b.Property(x => x.Version).HasMaxLength(ModelConsts.MaxVersionLength).HasColumnName(nameof(Model.Version));
// Relations
b.HasMany<Export>(m => m.Exports).WithOne(e => e.Model).HasForeignKey(e => e.ModelId).IsRequired();
b.HasMany<Import>(m => m.Imports).WithOne(i => i.Model).HasForeignKey(i => i.ModelId).IsRequired();
b.HasMany<Source>(m => m.Sources).WithOne(s => s.Model).HasForeignKey(s => s.ModelId).IsRequired();
// Index
b.HasIndex(x => new { x.SchemaName });
b.Navigation(x => x.Exports).HasField("_exports");
b.Metadata.FindNavigation("Exports").SetPropertyAccessMode(PropertyAccessMode.Field);
});
builder.Entity<Export>(b =>
{
b.ToTable(OdmsDbProperties.DbTablePrefix + "Exports", OdmsDbProperties.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.ModelId).HasColumnName(nameof(Export.ModelId)).IsRequired();
b.Property(x => x.OperationId).HasColumnName(nameof(Export.OperationId)).IsRequired();
b.Property(x => x.ExportType).HasMaxLength(ExportConsts.MaxExportTypeLength).HasColumnName(nameof(Export.ExportType)).IsRequired();
b.Property(x => x.Result).HasMaxLength(ExportConsts.MaxResultLength).HasColumnName(nameof(Export.Result)).IsRequired();
// Value object
b.OwnsOne(x => x.ExportFile, p =>
{
p.Property(x => x.StorageId).HasColumnName(ExportConsts.ExportFileIdColumnName);
p.Property(x => x.Name).HasColumnName(ExportConsts.ExportFileNameColumnName);
p.Ignore(x => x.NameOnly);
p.Ignore(x => x.FullName);
p.Ignore(x => x.ModelType);
// Index
p.HasIndex(x => x.Name);
}).Navigation(x => x.ExportFile).IsRequired();
});
**Domain Manager layer inside : **
public virtual async Task HardDeleteExportAsync(string schemaName, string serverName, Guid fileId)
{
Check.NotNullOrWhiteSpace(schemaName, nameof(schemaName), ModelConsts.MaxSchemaNameLength);
Check.NotNullOrWhiteSpace(serverName, nameof(serverName), ModelConsts.MaxServerNameLength);
// Get model from database with conditional exports
var model = await ModelRepository.FindWithExportDetailAsync(
schemaName,
serverName,
x => x.IsDeleted == true && x.ExportFile.StorageId == fileId,
includeDetails: true // includeDetails: Set true to include all children of this aggregate
);
if (model == null)
{
throw new ModelDoesNotExistException(
schemaName: schemaName,
serverName: serverName
);
}
//NOTE => below code not working if we have a cascade delete relation but we want to delete only children , not with parent. we need to do it from repository layer
model.RemoveAllExports(); //model.HardDeleteExport(fileId);
await ModelRepository.UpdateAsync(model,true);
}
FindWithExportDetailAsync inside which is inside repository layer :
public virtual async Task<Model> FindWithExportDetailAsync(string schemaName, string serverName, Expression<Func<Export, bool>> expression, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.IncludeExportDetail(expression, includeDetails) // Include only exports
.Where(x => x.SchemaName == schemaName && x.ServerName == serverName)
.FirstOrDefaultAsync(GetCancellationToken(cancellationToken)); // Returns null if not found
}
IncludeExportDetail method inside :
public static IQueryable<Model> IncludeExportDetail(this IQueryable<Model> queryable, Expression<Func<Export, bool>> predicate, bool include = true)
{
if (!include)
{
return queryable;
}
return queryable
.Include(
x => x.Exports.AsQueryable()
.Where(predicate));
}```
Model aggregate root and removeExport method
public class Model : AuditedAggregateRoot<Guid>, IMultiTenant // Using Guid type as the Id key
{
public Guid? TenantId { get; protected set; }
[NotNull]
public virtual string SchemaName { get; protected set; } // Value object can be created for primitive types. There is no such requirement in the web API. Inputs are validated in the HTTP layer.
[NotNull]
public virtual string ServerName { get; protected set; } // Value object can be created for primitive types. There is no such requirement in the web API. Inputs are validated in the HTTP layer.
public virtual DatabaseType DatabaseType { get; protected set; }
public virtual string Password { get; protected set; }
[NotNull]
public virtual string Version { get; protected set; }
// Don't expose mutable collections in an aggregate
public virtual IReadOnlyCollection<Export> Exports
{
get
{
return _exports?.ToList(); // Paged operation may return without sub collection. If null then do not turn into list
}
}
private readonly ICollection<Export> _exports;
public virtual void RemoveAllExports()
{
// NOTE => Clear ,or new, or removeall not working when dealing with ef core because of it only clear the list , and parent doesn't know about the relational children deletion.
_exports.Clear();
}
}
Yes, you should already update the aggregate root normally after children entities changes. So I've tried and that's the problem which is not working even if updating aggregate root. I don't think it's because of private field because we are using backing fields actually , and in a normal ef core project it's working as expected for example :
Entity:
And the operation below is working :
Because of this simple ef core project is working without problem , we think that if this issue about abp efcore implementation? I can show our configuration anytime , adding and saving changes working perfectly but in deletion , removing step with that backing fields , could it be an issue ?
Don't understand the last message , you'll be checking the issue right ?
Thanks , I've already tried this approach but this time no hangfire jobs are registered. So for now I'll be waiting for the other issue solution I think. As a result of this topic , I understand that we shouldn't override a module if there isn't any problem, right ? Thank you.
Ok thank you, we'll be calling SetAsync() method before.
Actually I don't want to change options I want to change for example OnApplicationInitialization method behaviour because of an internal module error . I've also opened the issue related with this topic , you can check the specific case from below link :
https://support.abp.io/QA/Questions/2578/AbpBackgroundWorkersHangfireModule-exception-without-using-hangfire-configuration
But I'll try your module approach. Note => If the issue in the link above will be fixing , we don't even need to change the module.
Hello , Normally I shouldn't reach the entity repository like that from DDD manner , It's not a best practice. Because I should delete them from aggregate repository, so normally we don't have any repositories for entity itself. Anyway I've tried this approach , too. But it didn't work.
Ok, thanks for the information.
Is this the abp design or it comes from Ef core ORM usage ? Because normally when you directly listen table changes from sql we can catch the changes. I understand that abp's pre-built events do not support this cascade deletion , right ?
Hello , Is there any spesific reason to choose netstandard as target framework ? If we change all layers to .net5 , will we run into an unexpected error ?
Thank you.