使用 DBContext ChangeTracker 编写基于事件的 SignalR 通知服务 - 关注点分离

Writing an event based SignalR Notification Service using DBContext ChangeTracker - separation of concerns

我有一个控制器可以修改日历中的约会。我想使用我的 SignalR 集线器通知用户 à la "User X changed {appointmentTitle}: List: {Property} {OriginalValue} {NewValue}"

我是 C# 的初学者(语法方面没问题,但 OOP 概念是新的);我正在尝试使用事件来实现上述目标。 下面是处理程序和参数、控制器的摘录和我的问题摘要。

代码已缩写!

事件参数

    public class AppointmentChangeEventArgs : EventArgs
    {
        public EntityState AppointmentState = EntityState.Unchanged;
        public EntityEntry Entity = null;
        public ScheduleData Appointment = null;
    }

事件处理器

    // maybe this could be just one, and let the consumer decide based on EntityState?
    public EventHandler<AppointmentChangeEventArgs> AppointmentChanged;
    public EventHandler<AppointmentChangeEventArgs> AppointmentAdded;
    public EventHandler<AppointmentChangeEventArgs> AppointmentRemoved;

    protected virtual void OnAppointment(AppointmentChangeEventArgs appointmentChangeEventArgs)
    {
        switch (appointmentChangeEventArgs.AppointmentState)
        {
            case EntityState.Added:
                AppointmentAdded?.Invoke(this, appointmentChangeEventArgs);
                break;
            case EntityState.Deleted:
                AppointmentRemoved?.Invoke(this, appointmentChangeEventArgs);
                break;
            case EntityState.Modified:
                AppointmentChanged?.Invoke(this, appointmentChangeEventArgs);
                break;
            default:
                break;
        }
    }

控制器

public async Task<IActionResult> Batch([FromBody] ScheduleEditParameters param)
    switch (param.Action) {
        case "insert":
             await _dbContext.Appointments.AddAsync(appointment);
             break;
        case "update":
             // .. get Appointment from DB
             appointment.Subject = value.Subject;
             appointment.StartTime = value.StartTime;
             // ...
        case "remove": 
             // .. get Appointment from DB
             _dbContext.Appointments.Remove(appointment);
    }
    var modifiedEntries = _dbContext.ChangeTracker
            .Entries()
            .Where(x => x.State != EntityState.Unchanged && x.State != EntityState.Detached)
            .Select(x => new AppointmentChangeEventArgs() { Entity  = (EntityEntry) x.Entity, AppointmentState = x.State, Appointment = appointment })
            .ToList();

        if (modifiedEntries.Any())
        {
            var notificationService = new NotificationService(signalRHub, notificationLogger);
            AppointmentAdded += notificationService.OnAppointmentChanged;
            AppointmentChanged += notificationService.OnAppointmentChanged;
            AppointmentRemoved += notificationService.OnAppointmentChanged;
        }
        await _dbContext.SaveChangesAsync();

问题

如果您能深入了解如何构建和处理此任务,我将不胜感激。谢谢。

首先,我通常建议您不要在这里使用事件。事件听起来可能非常有用,但由于它们的工作方式(同步),它们并不是真正在 Web 上下文中实现此目的的最佳方式,尤其是在像 ASP.NET Core 这样的主要异步框架中。

相反,我建议您简单地声明您自己的类型,例如IAppointmentChangeHandler 像这样:

public interface IAppointmentChangeHandler
{
    Task AddAppointment(ScheduleData appointment);
    Task UpdateAppointment(ScheduleData appointment);
    Task RemoveAppointment(ScheduleData appointment);
}

您的 NotificationService 只需 实现 该接口即可处理这些事件(显然只需发送您需要发送的任何内容):

public class NotificationService : IAppointmentChangeHandler
{
    private readonly IHubContext _hubContext;

    public NotificationService(IHubContext hubContext)
    {
        _hubContext = hubContext;
    }

    public AddAppointment(ScheduleData appointment)
    {
        await _hubContext.Clients.InvokeAsync("AddAppointment", appointment);
    }

    public UpdateAppointment(ScheduleData appointment)
    {
        await _hubContext.Clients.InvokeAsync("UpdateAppointment", appointment);
    }

    public RemoveAppointment(ScheduleData appointment)
    {
        await _hubContext.Clients.InvokeAsync("RemoveAppointment", appointment);
    }
}

在你的控制器内部,你只需注入 IAppointmentChangeHandler 然后调用它的实际方法。这样你就可以让控制器和通知服务完全分离:控制器不需要先构造类型,你也不需要订阅一些事件(你也可以必须在某个时候再次取消订阅 btw)。并且完全可以把实例化交给DI容器。


回答您的个别问题:

Is it ok to use EntityEntry and EntityState in event arguments?

我会避免在数据库之外的上下文中使用它。两者都是数据库设置的实现细节,因为您在这里使用 Entity Framework。这不仅会使您的事件处理程序与 Entity Framework 紧密耦合(这意味着每个想成为事件处理程序的人都需要引用 EF,即使他们没有对它做任何事情),您还可能泄漏内部状态稍后可能会更改(您不 拥有 EntityEntry 所以谁知道 EF 之后会用它做什么)。

for each modified Entry, I can obtain _dbContext.Entry(modifiedEntry).Properties.Where(x => x.IsModified).ToList();

如果您查看代码,您首先会在数据库集上调用 AddUpdateRemove;然后你使用一些逻辑来查看一些 内部 EF 的东西来找出完全相同的东西。如果您直接在这三个 switch 案例中构建 AppointmentChangeEventArgs,您可以使它变得不那么复杂。

but does this belong in the NotificationService class? In order to do that, I'd also need to pass the DbContext over to NotificationService.

通知服务与数据库有什么关系吗?我会说不;除非您将这些通知保存到数据库中。当我想到通知服务时,我希望能够调用它的某些东西来主动触发通知,而不是在服务中使用一些逻辑来确定它可能触发什么通知。

Might there be a simpler way to achieve this? Adding and Removing handlers are easy ("User X has added|removed ... appointment {Title}"), but in order to figure out the exact changes I'll have to look at the modified properties.

先用最简单的方式想一想:你在哪里更新数据库实体的值?在那个 update 案例中。所以在那一点上,当你从传递的对象中复制值时,你也可以只检查你实际更改的属性。这样,您就可以轻松记录需要通知的属性。

将它与 EF 完全分离,在长期 运行.

中你会更加灵活