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.

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.