Can I run IDataSeedContributor implementations in a prescribed order?

Viewed 181

I am trying to create some simple sample data in my database. I have coded several IDataSeedContributorimplementations. In the implementations I inject and call multiple repositories to retrieve data seeded from my other implementations. I want to use that seeded data to populate the dependent properties of the entity in the current implementation.

Here is an example:

public ContactsDataSeedContributor(
    IDepartmentRepository departmentRepository,
    IContactRepository contactRepository,
    IAddressRepository addressRepository,
    IEmailAddressRepository emailAddressRepository,
    IPhoneNumberRepository phoneNumberRepository,
    IGuidGenerator guidGenerator,
    ICurrentTenant currentTenant)
{
    _departmentRepository = departmentRepository;
    _contactRepository = contactRepository;
    _addressRepository = addressRepository;
    _emailAddressRepository = emailAddressRepository;
    _phoneNumberRepository = phoneNumberRepository;
    _guidGenerator = guidGenerator;
    _currentTenant = currentTenant;
}

public async Task SeedAsync(DataSeedContext context)
{
    using (_currentTenant.Change(context?.TenantId))
    {

        if (!(await _contactRepository.AnyAsync(x => x.TenantId.ToString() == _currentTenant.Id.ToString())))
        {
            var department = (await _departmentRepository.GetListAsync()).FirstOrDefault();
            var contact = new Contact
            (
                id: _guidGenerator.Create(),
                name: "",
                surname: "Sample Contact",
                title: "Sample Title",
                lastContactDate: DateTime.UtcNow,
                note: "This is a sample note.",
                departmentId: department.Id,
                clientId: null,
                primary: true,
                active: true
            );
            (await _addressRepository.GetListAsync()).ForEach(x => contact.Addresses.Add(x));
            (await _emailAddressRepository.GetListAsync()).ForEach(x => contact.EmailAddresses.Add(x));
            (await _phoneNumberRepository.GetListAsync()).ForEach(x => contact.PhoneNumbers.Add(x));
            await _contactRepository.InsertAsync(contact);
        }

    }
}

I was anticipating and I have gotten several exceptions that demonstrate that the dependent data has not yet been initialized. That seems to suggest that there is not an intelligent mechanism for determining the order that the implementations should run.

Is there any way to give the framework a hint?

Can I run IDataSeedContributor implementations in a prescribed order?

I was trying to avoid a monolithic top-down implementation with everything running in my prescribed order.

Here is a class diagram for what I am trying to initialize:

Class Diagram

2 Answers

I believe you can do this by injecting IDataSeeder instead of inheriting it from IDataSeedContributor directly. Because now you have complete control, you can call the SeedAsync method whenever and wherever you want.

I hope this information will be useful and you will achieve what you want in a short time.

References

While I appreciate that @berkansasmaz put me on the right path, it was not a complete solution. I thought I would share some of my solution here in hopes of helping someone else.

All of the examples given in the ABP documentation are for initializing entities that have no dependency on other entities. I wanted to create sample data so that when I create a new tenant, that tenant would already have something to work with.

My solution was to modify the provided SaasDataSeedContributor to include my own ITenantDataSeeder implementation.

SaasDataSeedContributor modified to include an injected ITenantDataSeeder

public interface ITenantDataSeeder
{
  Task CreateTenantAsync();
}

public class TenantDataSeeder : ITenantDataSeeder, ITransientDependency
{
  private readonly ICurrentTenant _currentTenant;
  private readonly ILogger<TenantDataSeeder> _logger;
  private readonly ICountryDataSeeder _countryDataSeeder;
  // other private fields here

  public TenantDataSeeder(
    ICountryDataSeeder countryDataSeeder,
    IStateDataSeeder stateDataSeeder,
    IAddressDataSeeder addressDataSeeder,
    IEmailAddressDataSeeder emailAddressDataSeeder,
    IPhoneNumberDataSeeder phoneNumberDataSeeder,
    IDepartmentDataSeeder departmentDataSeeder,
    IContactDataSeeder contactDataSeeder,
    ICampaignDataSeeder campaignDataSeeder,
    ILeadSourceDataSeeder leadSourceDataSeeder,
    IProductGroupDataSeeder productGroupDataSeeder,
    IProductDataSeeder productDataSeeder,
    IClientNeedDataSeeder clientNeedDataSeeder,
    IRevenueRepeatFreqDataSeeder repeatFreqDataSeeder,
    IOpportunityStatusDataSeeder opportunityStatusDataSeeder,
    IUserDataSeeder userDataSeeder,
    IClientDataSeeder clientDataSeeder,
    IOpportunityDataSeeder opportunityDataSeeder,
    IOpportunityLineItemDataSeeder opportunityLineItemDataSeeder,
    ISalesNoteDataSeeder salesNoteDataSeeder,
    ICurrentTenant currentTenant,
    ILogger<TenantDataSeeder> logger)
  {
    // private fields initialized here
  }

  public async Task CreateTenantAsync()
  {
    // Host Seeding
    await _countryDataSeeder.CreateCountriesAsync();

    if (!_currentTenant.IsAvailable)
      return;

    // Tenant Seeding
    try
    {
      await _stateDataSeeder.CreateStatesAsync();
      await _addressDataSeeder.CreateAddressesAsync();
      await _emailAddressDataSeeder.CreateEmailAddressesAsync();
      await _phoneNumberDataSeeder.CreatePhoneNumbersAsync();
      await _departmentDataSeeder.CreateDepartmentsAsync();
      await _contactDataSeeder.CreateContactsAsync();
      await _campaignDataSeeder.CreateCampaignsAsync();
      await _leadSourceDataSeeder.CreateLeadSourcesAsync();
      await _productGroupDataSeeder.CreateProductGroupsAsync();
      await _productDataSeeder.CreateProductsAsync();
      await _clientNeedDataSeeder.CreateClientNeedsAsync();
      await _repeatFreqDataSeeder.CreateRevenueRepeatFreqsAsync();
      await _opportunityStatusDataSeeder.CreateOpportunityStatusesAsync();
      //await _userDataSeeder.CreateUsersAsync();
      await _clientDataSeeder.CreateClientsAsync();
      await _opportunityDataSeeder.CreateOpportunitiesAsync();
      await _opportunityLineItemDataSeeder.CreateOpportunityLineItemsAsync();
      await _salesNoteDataSeeder.CreateSalesNotesAsync();
    }
    catch (ApplicationException e)
    {
      _logger.LogException(e, LogLevel.Warning);
    }
  }
}

And then I wrote several <Entity>DataSeeder interfaces and implementations.

NOTE that these classes DO NOT implement the IDataSeeder interface nor do they implement the IDataSeedContributor interface. I probably should re-factor them to be named <Entity>DataSeederFragmentor something equivalent to avoid confusion.

Here is an example of one of the interfaces and its implementation:

public interface IStateDataSeeder
{
  Task<IEnumerable<State>> CreateStatesAsync();
}

public class StateDataSeeder : IStateDataSeeder, ITransientDependency
{
  protected static IList<State> States = new List<State>();

  private readonly ICurrentTenant _currentTenant;
  private readonly IGuidGenerator _guidGenerator;
  private readonly ICountryDataSeeder _countryDataSeeder;
  private readonly IStateRepository _stateRepository;

  public StateDataSeeder(
    IStateRepository stateRepository,
    ICountryDataSeeder countryDataSeeder,
    IGuidGenerator guidGenerator,
    ICurrentTenant currentTenant)
  {
    _stateRepository = stateRepository;
    _countryDataSeeder = countryDataSeeder;
    _guidGenerator = guidGenerator;
    _currentTenant = currentTenant;
  }

  public async Task<IEnumerable<State>> CreateStatesAsync()
  {
    if (!_currentTenant.IsAvailable)
      return Enumerable.Empty<State>();

    try
    {
      var states = _stateRepository.Where(a => a.TenantId == _currentTenant.Id);
      if (states.Any()) return states;
    }
    catch (NullReferenceException e)
    {
      throw new ApplicationException($"Separated tenant database not yet initialized. Run the DbMigrator."); 
    }

    if (States.Any()) return States;

    var usId = Guid.Parse(CountryConsts.IdUnitedStates);

    var countries = await _countryDataSeeder.CreateCountriesAsync();
    var usCountry = countries.FirstOrDefault(c => c.Id == usId);
    if (usCountry == null)
      throw new ApplicationException("Seeded Country United States was null.");

    States.Add(await _stateRepository.InsertAsync(new State
    (
        id: _guidGenerator.Create(),
        name: "Alabama",
        code: "AL",
        primary: false,
        active: true,
        countryId: usId
    ), autoSave: true));
    
    // other states here

    States.Add(await _stateRepository.InsertAsync(new State
    (
        _guidGenerator.Create(),
        name: "Wyoming",
        code: "WY",
        primary: false,
        active: true,
        countryId: usId
    ), autoSave: true));

    foreach (var state in States) state.TenantId = _currentTenant.Id;
    return States;
  }
}

NOTE: All of these interfaces and implementations were placed under my ProjectName.Domain project.

I am pretty sure that some of this could be done better. I debated about whether to use the static class field as a cache, but my downstream "data seeders" need to be able to access the states and the repository can't return them until they are committed by the [UnitOfWork].

I invite your constructive criticism. My purpose for posting this, however, was to help others like myself. I spent way too long trying to figure this out.

Related