Is it OK to seed data in the startup.cs? Are there any advantages, disadvantages to doing it there rather than in the DbContext or Migration Configuration Class?
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
SeedData();
}
public void SeedData()
{
using (ApplicationDbContext _db = new ApplicationDbContext()) {
if (!_db.Books.Any(n => n.Name == "Seeded in Startup.cs"))
{
_db.Books.Add(new Book() { Name = "Seeded in Startup.cs", Date = "1966" });
}
_db.SaveChanges();
}
}
We created a SeedHelpers.cs
In this file all seed data are available.
public async Task Seed()
{
try
{
if (RoleManager.Roles.ToList().Count == 0)
foreach (string role in typeof(RoleConst).GetConstValue<string>())
await RoleManager.CreateAsync(new RoleEntity { Name = role.ToString() });
if (UserManager.Users.ToList().Count == 0)
{
UserEntity entity = new UserEntity
{
Email = "y",
Active = true,
Deleted = false,
EmailConfirmed = true,
Created = DateTime.UtcNow,
Modified = DateTime.UtcNow,
Name = "y",
UserName = "x"
};
await UserManager.CreateAsync(entity, "fg#123");
await UserManager.AddToRoleAsync(entity, RoleConst.Admin);
//Send Invitation email to Admin in the Production.
}
if(DapperLanguage.All().ToList().Count()==0)
{
await DapperLanguage.AddAsync(new Language
{
Id = Guid.NewGuid(),
Code = LanguageConst.English,
Name = "English"
});
await DapperLanguage.AddAsync(new Language
{
Id = Guid.NewGuid(),
Code = LanguageConst.Arabic,
Name = "Arabic"
});
}
}
catch (Exception ex)
{
LogManager.LogError(JsonConvert.SerializeObject(new { class_name = this.GetType().Name, exception = ex }));
}
}
And In Startup.cs file
public void Configuration(IAppBuilder app, Helpers.SeedHelpers seed)
{
ConfigureAuth(app);
seed.Seed().Wait();
}
Try this type of method.
If you are using ef core you can now use HasData() in the ef mapping configurations:
https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding
If using ef 6 you can set an initialiser on the DbContext and maybe store the seed data for each entity in a separate file in a separate project?
http://www.entityframeworktutorial.net/code-first/seed-database-in-code-first.aspx
Related
Working sample at the end of this article (I kept all things I tried [reason for long article], so that others could benefit from it later)
I am trying to write integration tests for my EF Core 3.1 class library. As unit test framework, I have used XUnit and followed guide from Microsoft: https://learn.microsoft.com/en-us/ef/core/testing/sharing-databases
Here is how the setup looks like (it's a bit longer because I am actually creating a database in my SQL Server in case I need to see the real result from tests output):
public class SharedDatabaseFixture : IDisposable
{
private static readonly object _lock = new object();
private static bool _databaseInitialized;
private static string _DatabaseName = "Database.Server.Local";
private static IConfigurationRoot config;
public SharedDatabaseFixture()
{
config = new ConfigurationBuilder()
.AddJsonFile($"appsettings.Development.json", true, true)
.Build();
var test = config.GetValue<string>("DataSource");
var connectionStringBuilder = new SqlConnectionStringBuilder
{
DataSource = config.GetValue<string>("DataSource"),
InitialCatalog = _DatabaseName,
IntegratedSecurity = true,
};
var connectionString = connectionStringBuilder.ToString();
Connection = new SqlConnection(connectionString);
CreateEmptyDatabaseAndSeedData();
Connection.Open();
}
public bool ShouldSeedActualData { get; set; } = true;
public DbConnection Connection { get; set; }
public ApplicationDbContext CreateContext(DbTransaction transaction = null)
{
var identity = new GenericIdentity("admin#sample.com", "Admin");
var contextUser = new ClaimsPrincipal(identity); //add claims as needed
var httpContext = new DefaultHttpContext() { User = contextUser };
var defaultHttpContextAccessor = new HttpContextAccessor();
defaultHttpContextAccessor.HttpContext = httpContext;
var context = new ApplicationDbContext(new DbContextOptionsBuilder<ApplicationDbContext>().UseSqlServer(Connection).Options, null, defaultHttpContextAccessor);
if (transaction != null)
{
context.Database.UseTransaction(transaction);
}
return context;
}
private static void ExecuteSqlCommand(SqlConnectionStringBuilder connectionStringBuilder, string commandText)
{
using (var connection = new SqlConnection(connectionStringBuilder.ConnectionString))
{
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandText = commandText;
command.ExecuteNonQuery();
}
}
}
private static SqlConnectionStringBuilder Master => new SqlConnectionStringBuilder
{
DataSource = config.GetValue<string>("DataSource"),
InitialCatalog = "master",
IntegratedSecurity = true
};
private static string Filename => Path.Combine(Path.GetDirectoryName(typeof(SharedDatabaseFixture).GetTypeInfo().Assembly.Location), $"{_DatabaseName}.mdf");
private static string LogFilename => Path.Combine(Path.GetDirectoryName(typeof(SharedDatabaseFixture).GetTypeInfo().Assembly.Location), $"{_DatabaseName}_log.ldf");
private static void CreateDatabaseRawSQL()
{
ExecuteSqlCommand(Master, $#"IF(db_id(N'{_DatabaseName}') IS NULL) BEGIN CREATE DATABASE [{_DatabaseName}] ON (NAME = '{_DatabaseName}', FILENAME = '{Filename}') END");
}
private static List<T> ExecuteSqlQuery<T>(SqlConnectionStringBuilder connectionStringBuilder, string queryText, Func<SqlDataReader, T> read)
{
var result = new List<T>();
using (var connection = new SqlConnection(connectionStringBuilder.ConnectionString))
{
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandText = queryText;
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
result.Add(read(reader));
}
}
}
}
return result;
}
private static void DestroyDatabaseRawSQL()
{
var fileNames = ExecuteSqlQuery(Master, $#"SELECT [physical_name] FROM [sys].[master_files] WHERE [database_id] = DB_ID('{_DatabaseName}')", row => (string)row["physical_name"]);
if (fileNames.Any())
{
ExecuteSqlCommand(Master, $#"ALTER DATABASE [{_DatabaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;EXEC sp_detach_db '{_DatabaseName}', 'true'");
fileNames.ForEach(File.Delete);
}
if (File.Exists(Filename))
File.Delete(Filename);
if (File.Exists(LogFilename))
File.Delete(LogFilename);
}
private void CreateEmptyDatabaseAndSeedData()
{
lock (_lock)
{
if (!_databaseInitialized)
{
using (var context = CreateContext())
{
try
{
DestroyDatabaseRawSQL();
}
catch (Exception) { }
try
{
CreateDatabaseRawSQL();
context.Database.EnsureCreated();
}
catch (Exception) { }
if (ShouldSeedActualData)
{
List<UserDB> entities = new List<UserDB>()
{
new UserDB() { Id = "Admin#sample.com", Name= "Admin" }
};
context.Users.AddRange(entities);
context.SaveChanges();
List<IdentityRole> roles = new List<IdentityRole>()
{
new IdentityRole(){Id = "ADMIN",Name = nameof(DefaultRoles.Admin), NormalizedName = nameof(DefaultRoles.Admin)},
new IdentityRole(){Id = "FINANCE",Name = nameof(DefaultRoles.Finance), NormalizedName = nameof(DefaultRoles.Finance)}
};
context.Roles.AddRange(roles);
context.SaveChanges();
}
}
_databaseInitialized = true;
}
}
}
public void Dispose()
{
Connection.Dispose();
}
}
Then, test class looks like following (for simplicity showing just 2 tests):
public class BaseRepositoryTests : IClassFixture<SharedDatabaseFixture>
{
private readonly SharedDatabaseFixture fixture;
private IMapper _mapper;
public BaseRepositoryTests(SharedDatabaseFixture fixture)
{
this.fixture = fixture;
var config = new MapperConfiguration(opts =>
{
opts.AddProfile<CountriesDBMapper>();
opts.AddProfile<EmployeeDBMapper>();
opts.AddProfile<EmployeeAccountDBMapper>();
});
_mapper = config.CreateMapper();
}
[Fact]
public async Task EntityCannotBeSavedIfDbEntityIsNotValid()
{
using (var transaction = fixture.Connection.BeginTransaction())
{
using (var context = fixture.CreateContext(transaction))
{
var baseCountryRepository = new BaseRepository<CountryDB, Country>(context, _mapper);
var invalidCountry = new Country() { };
//Act
var exception = await Assert.ThrowsAsync<DbUpdateException>(async () => await baseCountryRepository.CreateAsync(invalidCountry));
Assert.NotNull(exception.InnerException);
Assert.Contains("Cannot insert the value NULL into column", exception.InnerException.Message);
}
}
}
[Fact]
public async Task EntityCanBeSavedIfEntityIsValid()
{
using (var transaction = fixture.Connection.BeginTransaction())
{
using (var context = fixture.CreateContext(transaction))
{
var baseCountryRepository = new BaseRepository<CountryDB, Country>(context, _mapper);
var item = new Country() { Code = "SK", Name = "Slovakia" };
//Act
var result = await baseCountryRepository.CreateAsync(item);
Assert.NotNull(result);
Assert.Equal(1, result.Id);
}
}
}
}
Finally here is a sample of repository implementation (CRUD):
public async Task<TModel> CreateAsync(TModel data)
{
var newItem = mapper.Map<Tdb>(data);
var entity = await context.Set<Tdb>().AddAsync(newItem);
await context.SaveChangesAsync();
return mapper.Map<TModel>(entity.Entity);
}
public async Task<bool> DeleteAsync(long id)
{
var item = await context.Set<Tdb>().FindAsync(id).ConfigureAwait(false);
if (item == null)
throw new ArgumentNullException();
var result = context.Set<Tdb>().Remove(item);
await context.SaveChangesAsync();
return (result.State == EntityState.Deleted || result.State == EntityState.Detached);
}
If I run these tests individually, each one of them passes without a problem. However if I run all tests from BaseRepositoryTests then I am getting random problems as somehow, database transactions are not rolled back but the data is saved and shared between tests.
I have checked and truly, each transaction has it's own unique ID, thus they should not collide. Am I missing anything here? I mean according to Microsoft this is correct approach, but there is clearly something I have missed. The only thing different from other guides that I could find is, that I am using SaveChangesAsync in my repository implementation, while other's use SaveChanges...however I believe that this should not be the root cause of my problem.
Any help in respect to this matter would be highly appreciated.
Update 1:
As suggested by comments, I have tried two separate approaches. First one was to use CommitableTransaction like following:
Method update:
[Fact]
public async Task EntityCanBeSavedIfEntityIsValid()
{
using (var transaction = new CommittableTransaction(new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
using (var context = fixture.CreateContext(transaction))
{
var baseCountryRepository = new BaseRepository<CountryDB, Country>(context, _mapper);
var item = new Country() { Code = "SK", Name = "Slovakia" };
//Act
var result = await baseCountryRepository.CreateAsync(item);
Assert.NotNull(result);
Assert.Equal(1, result.Id);
}
}
}
Shared fixture update:
public ApplicationDbContext CreateContext(CommittableTransaction transaction = null)
{
... other code
if (transaction != null)
{
context.Database.EnlistTransaction(transaction);
}
return context;
}
This unfortunately ended with the same result when running my code tests in bulk (data that I was saving ended up being incremented and not discarded after each test)
Second thing I tried was using TransactionScope like following:
[Fact]
public async Task EntityCanBeModifiedIfEntityExistsAndIsValid()
{
using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadUncommitted }, TransactionScopeAsyncFlowOption.Enabled))
{
using (var context = fixture.CreateContext())
{
var baseCountryRepository = new BaseRepository<CountryDB, Country>(context, _mapper);
var item = new Country() { Code = "SK", Name = "Slovakia" };
//Act
var insertResult = await baseCountryRepository.CreateAsync(item);
Assert.NotNull(insertResult);
Assert.Equal(1, insertResult.Id);
Assert.Equal("SK", insertResult.Code);
Assert.Equal("Slovakia", insertResult.Name);
//Act
insertResult.Code = "SVK";
var result = await baseCountryRepository.UpdateAsync(insertResult.Id, insertResult);
Assert.Equal(1, result.Id);
Assert.Equal("SVK", result.Code);
Assert.Equal("Slovakia", result.Name);
}
scope.Complete();
}
}
Just as before this did not yield any new results.
Last thing that I tried was to drop :IClassFixture<SharedDatabaseFixture> from the test class and instead, create a new instance of my database fixture in constructor (which is being triggered for each test run) like following:
public BaseRepositoryTests()
{
this.fixture = new SharedDatabaseFixture();
var config = new MapperConfiguration(opts =>
{
opts.AddProfile<CountriesDBMapper>();
opts.AddProfile<EmployeeDBMapper>();
opts.AddProfile<EmployeeAccountDBMapper>();
});
_mapper = config.CreateMapper();
}
Just as before, no new results came from this update.
Working setup
Shared database fixture (basically class responsible for creating database...the main difference between previous version now is, that in constructor it accepts unique guid that is used when creating database -> to create a database with unique name. Furthermore I have also added a new method ForceDestroyDatabase() which is responsible to destroy the database after test has done it's job. I did not place it in Dispose() method, as sometimes you want to check what actually happened to database, where in that case you just don't call the method...see later)
public class SharedDatabaseFixture : IDisposable
{
private static readonly object _lock = new object();
private static bool _databaseInitialized;
private string _DatabaseName = "FercamPortal.Server.Local.";
private static IConfigurationRoot config;
public SharedDatabaseFixture(string guid)
{
config = new ConfigurationBuilder()
.AddJsonFile($"appsettings.Development.json", true, true)
.Build();
var test = config.GetValue<string>("DataSource");
this._DatabaseName += guid;
var connectionStringBuilder = new SqlConnectionStringBuilder
{
DataSource = config.GetValue<string>("DataSource"),
InitialCatalog = _DatabaseName,
IntegratedSecurity = true,
};
var connectionString = connectionStringBuilder.ToString();
Connection = new SqlConnection(connectionString);
CreateEmptyDatabaseAndSeedData();
Connection.Open();
}
...other code the same as above, skipped for clarity
private void CreateEmptyDatabaseAndSeedData()
{
lock (_lock)
{
using (var context = CreateContext())
{
try
{
DestroyDatabaseRawSQL();
}
catch (Exception ex) { }
try
{
CreateDatabaseRawSQL();
context.Database.EnsureCreated();
}
catch (Exception) { }
if (ShouldSeedActualData)
{
List<UserDB> entities = new List<UserDB>()
{
new UserDB() { Id = "Robert_Jokl#swissre.com", Name= "Robert Moq" },
new UserDB() { Id = "Test_User#swissre.com", Name= "Test User" }
};
context.Users.AddRange(entities);
context.SaveChanges();
List<IdentityRole> roles = new List<IdentityRole>()
{
new IdentityRole(){Id = "ADMIN",Name = nameof(FercamDefaultRoles.Admin), NormalizedName = nameof(FercamDefaultRoles.Admin)},
new IdentityRole(){Id = "FINANCE",Name = nameof(FercamDefaultRoles.Finance), NormalizedName = nameof(FercamDefaultRoles.Finance)}
};
context.Roles.AddRange(roles);
context.SaveChanges();
}
}
}
}
public void ForceDestroyDatabase()
{
DestroyDatabaseRawSQL();
}
public void Dispose()
{
Connection.Close();
Connection.Dispose();
}
}
Sample test class:
public class DailyTransitDBRepositoryTests : IDisposable
{
private readonly SharedDatabaseFixture fixture;
private readonly ApplicationDbContext context;
private IMapper _mapper;
public DailyTransitDBRepositoryTests()
{
this.fixture = new SharedDatabaseFixture(Guid.NewGuid().ToString("N"));
this.context = this.fixture.CreateContext();
this.context.Database.OpenConnection();
var config = new MapperConfiguration(opts =>
{
opts.AddProfile<DailyTransitDBMapper>();
opts.AddProfile<EmployeeDBMapper>();
opts.AddProfile<EmployeeAccountDBMapper>();
opts.AddProfile<CountriesDBMapper>();
});
_mapper = config.CreateMapper();
}
...other code ommited for clarity
public void Dispose()
{
this.context.Database.CloseConnection();
this.context.Dispose();
this.fixture.ForceDestroyDatabase();
this.fixture.Dispose();
}
[Fact]
public async Task GetTransitsForYearAndMonthOnlyReturnsValidItems()
{
var employees = await PopulateEmployeesAndReturnThemAsList(context);
var countries = await PopulateCountriesAndReturnThemAsList(context);
var transitRepository = new DailyTransitDBRepository(context, _mapper);
var transitItems = new List<DailyTransit>() {
new DailyTransit()
{
Country = countries.First(),
Employee = employees.First(),
Date = DateTime.Now,
TransitionDurationType = DailyTransitDurationEnum.FullDay
},
new DailyTransit()
{
Country = countries.First(),
Employee = employees.Last(),
Date = DateTime.Now.AddDays(1),
TransitionDurationType = DailyTransitDurationEnum.FullDay
},
new DailyTransit()
{
Country = countries.First(),
Employee = employees.Last(),
Date = DateTime.Now.AddMonths(1),
TransitionDurationType = DailyTransitDurationEnum.FullDay
}
};
//Act
await transitRepository.CreateRangeAsync(transitItems);
//retrieve all items
using (var context2 = fixture.CreateContext())
{
var transitRepository2 = new DailyTransitDBRepository(context2, _mapper);
var items = await transitRepository2.GetEmployeeTransitsForYearAndMonth(DateTime.Now.Year, DateTime.Now.Month);
Assert.Equal(2, items.Count());
Assert.Equal("Janko", items.First().Employee.Name);
Assert.Equal("John", items.Last().Employee.Name);
}
}
}
Robert, Glad It helped! As per your request, I re-submit the answer for anyone that could find this answer helpful as you.
I learn the hard way that trying to share the entity framework database context over IClassFixture or CollectionFixtures would eventually end up in tests being polluted with another test data or deadlock/race conditions due to the parallel execution of xUnit, entity framework throwing exceptions because it already tracked that object with a given Id and more headaches like that.
Personally, I would kindly recommend that for your specific use cause, stick the database context creation/cleanup within the constructor/dispose alternative such as:
public class TestClass : IDisposable
{
DatabaseContext DatabaseContext;
public TestClass()
{
var options = new DbContextOptionsBuilder<DatabaseContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
DatabaseContext = new DatabaseContext(options);
//insert the data that you want to be seeded for each test method:
DatabaseContext.Set<Product>().Add(new Product() { Id = 1, Name = Guid.NewGuid().ToString() });
DatabaseContext.SaveChanges();
}
[Fact]
public void FirstTest()
{
var product = DatabaseContext.Set<Product>().FirstOrDefault(x => x.Id == 1).Name;
//product evaluates to => 0f25a10b-1dfd-4b4b-a69d-4ec587fb465b
}
[Fact]
public void SecondTest()
{
var product = DatabaseContext.Set<Product>().FirstOrDefault(x => x.Id == 1).Name;
//product evaluates to => eb43d382-40a5-45d2-8da9-236d49b68c7a
//It's different from firstTest because is another object
}
public void Dispose()
{
DatabaseContext.Dispose();
}
}
Of course you can always do some refinement, but the idea is there
I have basically followed the whole tutorial from scratch, besides skipping javascript client. However, i copied some code from the sample Asp.Net Identity + EF Core combined and database with all tables was created successfully. However, when i run the identity server, there is no seed. I debugged the program.cs and the seed var is always false making it skipping the seed condition.
public class Program
{
public static void Main(string[] args)
{
var seed = args.Any(x => x == "/seed");
if (seed) args = args.Except(new[] { "/seed" }).ToArray();
var host = CreateWebHostBuilder(args).Build();
if (seed)
{
using (var scope = host.Services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
SeedData.EnsureSeedData(scope.ServiceProvider);
return;
}
}
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
this is the seed data class just like in the sample
public class SeedData
{
public static void EnsureSeedData(IServiceProvider provider)
{
provider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
provider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
provider.GetRequiredService<ConfigurationDbContext>().Database.Migrate();
{
var userMgr = provider.GetRequiredService<UserManager<ApplicationUser>>();
var alice = userMgr.FindByNameAsync("alice").Result;
if (alice== null)
{
alice = new ApplicationUser
{
UserName = "alice"
};
var result = userMgr.CreateAsync(alice, "Pass123$").Result
if (!result.Succeeded)
{
throw new Exception(result.Errors.First().Description);
}
alice = userMgr.FindByNameAsync("alice").Result;
result = userMgr.AddClaimsAsync(user, new Claim[]{
new Claim(JwtClaimTypes.Subject, "1"),
new Claim(JwtClaimTypes.Name, "Alice Smith"),
new Claim(JwtClaimTypes.GivenName, "Alice"),
new Claim(JwtClaimTypes.FamilyName, "Smith"),
new Claim(JwtClaimTypes.Email, "AliceSmith#email.com"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
new Claim(JwtClaimTypes.Role, "Admin")
}).Result;
if (!result.Succeeded)
{
throw new Exception(result.Errors.First().Description);
}
Console.WriteLine("user created");
}
else
{
Console.WriteLine("user already exists");
}
}
{
var context = provider.GetRequiredService<ConfigurationDbContext>();
if (!context.Clients.Any())
{
foreach (var client in Config.GetClients())
{
context.Clients.Add(client.ToEntity());
}
context.SaveChanges();
}
if (!context.IdentityResources.Any())
{
foreach (var resource in Config.GetIdentityResources())
{
context.IdentityResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
if (!context.ApiResources.Any())
{
foreach (var resource in Config.GetApis())
{
context.ApiResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
}
}
}
what am i missing? Everything code wise is the same, startup class too etc...
For passing string[] args, you need to launch project by Project.exe instead of IIS Express.
Follow Project Properties->Debug->Application Arguments->/seed->Launch Project from ProjectName.
In general, you may consider deciding seeding data by check wether there is any data in database instead of from command arguments.
I'm trying to fill the database with my StoreInitializer, but tables are empty, I'm using Entity Framework Core and ASP.NET Core, I want to configure my Initializer for using Update-Database in Package Manager Console. I followed this tutorial https://learn.microsoft.com/pl-pl/aspnet/core/data/ef-mvc/intro but on start or on updating, database is empty anyway. Visual studio shows me no errors.
StoreInitializer.cs :
namespace PC_SHOP.DAL
{
public static class StoreInitializer
{
public static void Initialize(StoreContext context)
{
context.Database.EnsureCreated();
if (context.Electronics.Any())
{
return;
}
// Działy główne
var electronics = new Electronic[]
{
new Electronic { ID=1, Name = "Laptopy"},
new Electronic { ID=2, Name = "Komputery"},
new Electronic { ID=3, Name = "Części PC"},
new Electronic { ID=4, Name = "Dla graczy"},
new Electronic { ID=5, Name = "Peryferia PC"},
new Electronic { ID=6, Name = "Sieci i komunikacja"},
new Electronic { ID=7, Name = "Oprogramowanie"},
};
foreach (Electronic e in electronics)
{
context.Electronics.Add(e);
}
context.SaveChanges();
// Rodzaje laptopów
var laptops = new List<Laptop>
{
new Laptop { ID=1, Name = "Laptopy", ElectronicID = 1, IconFileName = "1_laptop.jpg"},
new Laptop { ID=2, Name = "Laptopy Apple", ElectronicID = 1, IconFileName = "2_apple.jpg"},
};
foreach (Laptop l in laptops)
{
context.Laptops.Add(l);
}
context.SaveChanges();
}
}
}
StoreContext.cs :
namespace PC_SHOP.DAL
{
public class StoreContext : DbContext
{
public StoreContext(DbContextOptions<StoreContext> options) : base(options)
{
}
public DbSet<Electronic> Electronics { get; set; }
public DbSet<Laptop> Laptops { get; set; }
public DbSet<LaptopProduct> LaptopProducts { get; set; }
public DbSet<Employee> Employee { get; set; }
public DbSet<Project> Project { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Electronic>().ToTable("Electronic");
modelBuilder.Entity<Laptop>().ToTable("Laptop");
modelBuilder.Entity<LaptopProduct>().ToTable("LaptopProduct");
modelBuilder.Entity<Employee>().ToTable("Employee");
modelBuilder.Entity<Project>().ToTable("Project");
}
}
}
In ConfigurationServies in Startup.cs i added:
services.AddDbContext<StoreContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
and in appsettings.json:
"ConnectionStrings": {
"DefaultConnection": "Data Source=DESKTOP-J0OBBIO\\SQLEXPRESS;Initial Catalog=PC_SHOP;Integrated Security=SSPI;Database=Employee;Trusted_Connection=True;MultipleActiveResultSets=true"
},
Ok i fixed that,
I added context parameter to Configure method in Startup.cs file. Everything else have not been changed.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, StoreContext context )
{
...
//After app.UseMvc()
StoreInitializer.Initialize(context);
}
And it's filling my database correctly
Add this line to Program.cs Main() method :
StoreInitializer.Initialize(context);
public static void Main(string[] args)
{
//CreateWebHostBuilder(args).Build().Run();
var host = CreateWebHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<AppDbContext>();
//context.Database.EnsureCreated();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
host.Run();
}
EnsureCreated can be used to create the database and tables in prototyping and testing scenarios. It should be used instead of, not together with Migrations. It will only create the database if it does not exist or contains no tables. In other cases it will do nothing.
Note that if the database exists it will not do anything, so you need to use a database name that does not exist.
Really you should be generating migrations from the comand line then running the migrations from your initializer instead of using .EnsureCreated. EnsureCreated is for in memory database that is created each time. ie prototyping.
This question already has answers here:
How to seed in Entity Framework Core 2?
(9 answers)
Closed 4 years ago.
I am new to Ef Core. Trying to seed data while migration update.
He is how i did it
First created an initializer class:
using AcademicNet.Data.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AcademicNet.Data
{
public class PersonInitializer
{
private AcademicPortalContext _context;
public PersonInitializer(AcademicPortalContext context)
{
_context = context;
}
public async Task Seed()
{
if(!_context.PersonCategoryStatuses.Any())
{
_context.AddRange(_personCategoryStatus);
await _context.SaveChangesAsync();
}
if (!_context.PersonCategories.Any())
{
_context.AddRange(_personCategory);
await _context.SaveChangesAsync();
}
if (!_context.People.Any())
{
_context.AddRange(_people);
await _context.SaveChangesAsync();
}
}
List<PersonCategory> _personCategory = new List<PersonCategory>
{
new PersonCategory()
{
Name = "Internal Person Category",
Description = "Silahkan diganti redaksi ini",
PersonCategoryStatusId = 2,
StartDate = DateTime.UtcNow,
ModifiedDate = DateTime.UtcNow
}
};
List<Person> _people = new List<Person>
{
new Person()
{
FirstName = "Jannen",
LastName = "Siahaan",
Email = "j.siahaan#any.com",
DateAdd = DateTime.UtcNow,
ModifiedDate = DateTime.UtcNow
}
};
List<PersonCategoryStatus> _personCategoryStatus = new List<PersonCategoryStatus>
{
new PersonCategoryStatus()
{
Status = "Baru"
},
new PersonCategoryStatus()
{
Status = "Valid"
},
new PersonCategoryStatus()
{
Status = "Expired"
}
};
}
}
2. At the ConfigurationServices in Startup.cs add this:
services.AddTransient<PersonInitializer>();
At the parameter of Configure method at the Startup.cs add this injection:
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
PersonInitializer personSeeder,
AcademicPortalIdentityInitializer identitySeeder)
4. At the end of this "public void Configure" after app.UseMvc()
personSeeder.Seed().Wait();
suppose i have run the migration
and want to add a new PersonCategoryStatus as 'inValid' later. How can i acheive this.
new PersonCategoryStatus
List<PersonCategoryStatus> _personCategoryStatus = new List<PersonCategoryStatus>
{
new PersonCategoryStatus()
{
Status = "Baru"
},
new PersonCategoryStatus()
{
Status = "Valid"
},
new PersonCategoryStatus()
{
Status = "Expired"
},
new PersonCategoryStatus()
{
Status = "Invalid"
}
};
Any help is much appreciated. Thanks in advance
At Seed() body instead of this:
if(!_context.PersonCategoryStatuses.Any())
you should iterate over _personCategoryStatus collection and do a upsert operation for each possible status.
I have added an additional property to ApplicationUserRole as follows:
public class ApplicationUserRole : IdentityUserRole<int>
{
public string RoleAssigner { get; set; }
}
Now i am to assign a role to an user as follows:
[HttpPost]
public ActionResult Create(UserRoleViewModel userRoleViewModel)
{
if (ModelState.IsValid)
{
using (var context = new ApplicationDbContext())
{
var userRole = new ApplicationUserRole
{
UserId = userRoleViewModel.UserId,
RoleId = userRoleViewModel.RoleId,
RoleAssigner = userRoleViewModel.RoleAssigner
};
context.ApplicationUserRoles.Add(userRole);
context.SaveChanges();
return RedirectToAction("Index");
}
}
return View(userRoleViewModel);
}
This is working fine!!
Before adding additional "RoleAssigner" Property, I can assign a role to an user using AddToRoles() Method as follows:
[HttpPost]
public ActionResult Create(UserRoleViewModel userRoleViewModel)
{
if (ModelState.IsValid)
{
UserManager.AddToRoles(userRoleViewModel.Id, userRoleViewModel.RoleName);
return RedirectToAction("Index");
}
return View(userRoleViewModel);
}
My question is: After adding an additional Property like "RoleAssigner", Is there any way to assign a role to an user using AddToRoles() method which will also insert the additional "RoleAssigner" value for "RoleAssigner" column in the database also.
Edit with working example:
I think you can do that by creating an extension method at IdentityConfig.
I did something similar to find user by username or phone number
In what I'm able to understand you want to call UserManager.AddToRoles(...) and
fill the new role property.
to do that( in similar to the example before ) you need an extension to user manager. you do it like this:
public static class UserManagerExtens
{
public static IdentityResult AddToRole(this ApplicationUserManager userManager,string userId,string[] roles,string assigner)
{
try
{
ApplicationUserRole role = null;
using (ApplicationDbContext context = new ApplicationDbContext())
{
foreach (var item in roles)
{
role = new ApplicationUserRole();
role.UserId = userId;
role.RoleAssigner = assigner;
role.RoleId = item;
context.AspNetUserRoles.Add(role);
}
context.SaveChanges();
}
return new IdentityResult() { };
}
catch (Exception ex)
{
return new IdentityResult(ex.Message);
}
}
}
That is a working example, using UserManager you can call it with definded
parameters like:
string[] roles = new string[] { /*your roles here*/ };
UserManager.AddToRole(/*UserIdHere*/, roles, /*assigerId here*/);
Similar to this you can implement async or other methods of UserManager.
If you are using asp.net core application in startup.cs you should inject proper store models
services.AddIdentity<ApplicationUser, YOURROLEMODEL(ApplicationUserRole )>()
If you are using asp.net application there should be IdentityConfig.cs file You should implement your UserStore which will get you RoleModel as generic. You can see I have created AppUserStore class and it gets MyIdentityRole model as a generic Type. And changed ApplicationUserManager to use my AppUserStore class.
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
var manager = new ApplicationUserManager(new AppUserStore(context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
// Configure user lockout defaults
manager.UserLockoutEnabledByDefault = true;
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
manager.MaxFailedAccessAttemptsBeforeLockout = 5;
// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
// You can write your own provider and plug it in here.
manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider<ApplicationUser>
{
MessageFormat = "Your security code is {0}"
});
manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider<ApplicationUser>
{
Subject = "Security Code",
BodyFormat = "Your security code is {0}"
});
manager.EmailService = new EmailService();
manager.SmsService = new SmsService();
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
}
public class AppUserStore :
UserStore<ApplicationUser, MyIdentityRole, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>, IUserStore<ApplicationUser>
{
public AppUserStore(DbContext context) : base(context)
{
}
}
public class MyIdentityRole : IdentityRole
{
public string MyProperty { get; set; }
}