Я использую EF с DotNet Core 2.1 в своем приложении. Приложение работает с данными в нескольких связанных и связанных между собой таблицах FK.
Мне нужны данные журнала аудита только для изменений в одной таблице. Однако моя проблема в том, что таблица, которая мне нужна для аудита, имеет довольно много FK, и для каждого из них я хотел бы записать сам FK и поле из связанной таблицы.
Позвольте мне проиллюстрировать, о чем я говорю - предположим, это моя модель:
public class Blog {
public int Id { get; set; }
public string Name { get; set; }
public string Url { get; set; }
[InverseProperty ("Blog")]
public ICollection<Post> Posts { get; set; }
public Blog() {
Posts = new Collection<Post> ();
}
}
...
[AuditInclude]
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
[Required]
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
Как уже было сказано, я бы хотел, чтобы журнал аудита изменял только одну сущность - скажем, это Post
- вот класс аудита:
public class Audit_Post : IAudit {
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public string Blog { get; set; } // <- I need populating this from Blog.Name
[StringLength (64)]
public string AuditUsername { get; set; }
public DateTime AuditDt { get; set; }
public string AuditAction { get; set; }
public Audit_Manufacturer () { }
}
И вот как я настроил журнал аудита в моем startup.cs -> ConfigureService()
:
...
Audit.Core.Configuration.Setup ()
.UseEntityFramework (ef => ef
.AuditTypeExplicitMapper (m => m
.Map<Post, Audit_Post> ((d, al) => {
al.Blog = d.Blog?.Name; // !! This doesn't work
})
.AuditEntityAction<IAudit> ((evt, entry, auditEntity) => {
Object val;
var gotVal = evt.CustomFields.TryGetValue ("AuditUsername", out val);
string username = null;
if (gotVal && val is string)
username = val as string;
else
username = "<anonymous>";
auditEntity.AuditDt = DateTime.UtcNow;
auditEntity.AuditUsername = username;
auditEntity.AuditAction = entry.Action;
})
)
);
вопрос: Возможно ли вообще получать и проверять данные журнала из отношения зависимой таблицы (один ко многим)?
Помимо упомянутой проблемы, я также столкнулся с проблемой, не относящейся к теме, а именно - если я забуду обновить БД с миграцией для инициализации таблицы Audit_Posts
, и я выполняю операции с таблицей Posts
, данные сохраняются в позже, даже если журналы аудита не записываются (исключение сохранения UnitOfWork). Есть ли у AuditDbContext
флаг, который заставит его работать в той же транзакции, что и исходный запрос?
Ваш код должен работать нормально, если вы Include
свойство Blog
при извлечении объекта Post
, например:
using (var context = new BlogsContext())
{
var post = context.Posts
.Include(p => p.Blog) // Important, otherwise post.Blog will be NULL
.First();
post.Content += " adding this";
context.SaveChanges();
}
Если вы не можете включить объект Blog по какой-либо причине, вы можете выполнить запрос по сопоставлению, но вам нужно будет использовать перегрузку более низкого уровня для метода Map
, например:
.Map<Post, PostAudit>((ev, entry, postAudit) =>
{
var entryEf = entry.GetEntry();
var post = entryEf.Entity as Post;
var dbContext = entryEf.Context as BlogsContext;
// Get the blog related
var blog = dbContext.Blogs.FirstOrDefault(b => b.Id == post.BlogId);
postAudit.Blog = blog?.Name;
})
Что касается другого вопроса, в настоящее время нет встроенного механизма для отката изменений вашей базы данных, когда сохранение аудита не удается, но, возможно, вы попробуете с AuditDbContext
отменяет
С кодом Map<>((ev, entry, postAudit) => { ... })
я могу использовать только асинхронные методы на dbContext.Blogs
, но отображение не асинхронное, поэтому я не могу дождаться результата - я все еще пытаюсь понять, почему <<< Я чувствую себя новичком :)
Как отметил @ thepirat000, этого достаточно, чтобы гарантировать, что все связанные элементы присутствуют в памяти DbContext
. Это означает:
context.Posts.Add(item)
выполните запрос ко всем связанным элементам, таким как context.Blogs.Find(item.BlogId)
.Post
делайте это с .Include(d => d.Blog)
+ другими связанными элементами.Post
делайте это с .Include(d => d.Blog)
+ другими связанными элементами.Еще одна важная вещь, которая вызывала у меня проблемы, - это расположение моей таблицы аудита. Проблема заключалась в том, что я повторно использовал то же имя свойства в таблице аудита с другим типом - в исходной таблице свойство Blog
было свойством отношения, тогда как в таблице аудита это была строка. Это вызывало ошибки при преобразовании одной модели в другую.
[AuditInclude]
public class Post
{
...
[Required]
public int BlogId { get; set; }
public Blog Blog { get; set; }
...
}
Просто переименуйте его во что-нибудь, например:
public class Audit_Post
{
...
public int BlogId { get; set; }
public string BlogName { get; set; }
...
}
...
// and in startup.cs use ...
...
.Map<Post, Audit_Post> ((d, al) => {
al.BlogName = d.Blog?.Name;
})
...
По 2-му вопросу - ведение аудита внутри транзакций. Решил пока не использовать. Рассмотрю описанный случай тестами.
Может быть, предложение для будущего развития пакета - было бы хорошо, если бы упомянутые случаи были легко рассмотрены - я имею в виду, транзитивные свойства.
Спасибо за быстрый ответ. Я уже пробовал использовать
Include()
, но безуспешно. Я думал, это связано с тем, что это неselect
,update
илиdelete
. Это то, что я пробовал в моемPostRepository -> Add(Post item)
:context.Posts.Include(d => d.Blog).Add(item)
, но это привело к недопустимой операции, посколькуIIncludableQueryable<Post, Blog>
не содержит определения дляAdd()
. Я продолжаю расследование по этому поводу. Другое решение, которое вы предложили, вероятно, сработает - я собираюсь попробовать его прямо сейчас. Мне просто интересно, будет ли это намного дороже, так как это приведет к запросу +1.