Я пытаюсь реализовать более ориентированное на DDD решение для управления данными временных рядов. Следующие примеры кода и шаблоны можно найти здесь eShopOnWeb. По сути, это три сущности. Site
, Signal
и Sample
. Site
может иметь коллекцию Signals
, а Signal
может иметь коллекцию образцов.
public class Site: BaseEntity, IAggregateRoot
{
// Collection loaded by EFCore through Repository
private List<Signal> signals = new List<Signal>();
// Public read only access
public IEnumerable<Signal> Signals => this.signals.AsReadOnly();
}
public class Signal: BaseEntity, IAggregateRoot
{
// Signal has to belong to Site
public int SiteId { get; private set; }
// Typical EF Nav property removed
// Signal should have no access to it's 'parent' properties
// public Site Site { get; set;}
private List<Sample> samples = new List<Sample>();
public IEnumerable<Sample> Samples => this.samples.AsReadOnly();
}
public class Sample : BaseEntity
{
public int SignalId { get; private set; }
public DateTime TimeStamp { get; set; }
public double? Value { get; set; }
}
В качестве первого шага, борясь с отсутствием доступных книг Эванса или Вернона (они есть в посте), я остановился на двух AggregateRoot с Site
наиболее заметным. То есть Signal
агрегат действительно должен быть доступен через Site
.
Основная проблема, которую я обнаружил, связана с загрузкой подмножеств Samples
в Signal
.
В соответствии с шаблоном Specification
, используемым в примерах eShopOnWeb, я могу довольно легко работать с Site
агрегатом и загрузите его Signals
совокупную коллекцию с вызовом SiteRepository
на уровне Infrastructure
:
public sealed class SiteFilterSpecification : BaseSpecification<Site>
{
public SiteFilterSpecification(int id)
: base(s => s.Id == id)
{
this.AddInclude(s => s.Signals);
}
}
Если я нахожусь в Service
классе, где мне предоставили сайт и период времени, в течение которого что-то должно быть вычислено, обычно с несколькими Signals
, шаблон спецификации предложит что-то вроде:
public double GetComplexProcess(Site site, DateTime start, DateTime end)
{
var specification = new SiteSignalsWithSamplesSpec(site.Id, start, end);
var signals = this.SignalRepository.List(specification);
// signals should be loaded with the appropriate samples...
}
Проблема, которую я здесь обнаружил, заключается в том, что в спецификации невозможно отфильтровать Samples
, которые включены в Signal
public sealed class SiteSignalsWithSamplesSpecification : BaseSpecification<Signal>
{
public SiteSignalsWithSamplesSpecification(int siteId, DateTime from, DateTime end)
: base(s => s.SiteId == siteId)
{
// This throws exception at runtime
this.AddInclude(s => s.Samples.Where(sa => sa.TimeStamp >= from && sa.TimeStamp <= end));
}
}
Вы можете использовать этот подход и загрузить все Samples
, но при обработке данных временных рядов это может означать сотни тысяч сущностей, когда нам действительно нужен их сфокусированный выбор.
Чем я сейчас занимаюсь; и это не кажется особенно "чистым" - реализация версии класса Generic Repository специально для частичной загрузки Sample
данных в Signal
сущностях.
public interface ISignalRepository : IAsyncRepository<Signal>
{
Task<IEnumerable<Signal>> GetBySiteIdWithSamplesAsync(int siteId, DateTime from, DateTime to);
}
public class SignalRepository : EfRepository<Signal>, ISignalRepository
{
public SignalRepository(ForecastingContext dbContext) : base(dbContext)
{
}
public async Task<IEnumerable<Signal>> GetBySiteIdWithSamplesAsync(int siteId, DateTime from, DateTime to)
{
var signals = await this.dbContext.Signals.Where(s => s.SiteId == siteId).ToListAsync();
foreach (var signal in signals)
{
this.dbContext.Entry(signal)
.Collection(s => s.Samples)
.Query()
.Where(s => s.TimeStamp >= from && s.TimeStamp <= to)
.Load();
}
return signals;
}
}
Вероятно, это всего лишь начальная неуверенность, возникающая при развитии нового паттерна, но это почему-то кажется неправильным.
Верно ли, что я использую два агрегата?