How do you find or know where the "non-owned" entity type when trying to create a migration? - entity-framework-core

I have the following classes:
JobSeeker which owns a CreditCard which has a CreditCardType
public class JobSeeker : Entity
{
private readonly List<CreditCard> _creditCards;
public IEnumerable<CreditCard> CreditCards => _creditCards.AsReadOnly();
}
public class CreditCard : Entity
{
public CreditCardType CardType { get { return CreditCardType.From(_creditCardTypeID); } private set { } }
private readonly int _creditCardTypeID;}
public class CreditCardType : Enumeration
{
public static readonly CreditCardType Amex = new CreditCardType(1, nameof(Amex).ToLowerInvariant());
public static readonly CreditCardType Visa = new CreditCardType(2, nameof(Visa).ToLowerInvariant());
public static readonly CreditCardType MasterCard = new CreditCardType(3, nameof(MasterCard).ToLowerInvariant());
public static IEnumerable<CreditCardType> List() => new[] { Amex, Visa, MasterCard };}
My DBContext Configs are:
class JobSeekerEntityTypeConfiguration : IEntityTypeConfiguration<JobSeeker>
{
public void Configure(EntityTypeBuilder<JobSeeker> jsConfiguration)
{
if (jsConfiguration == null)
{
throw new ArgumentNullException(nameof(jsConfiguration));
}
// Build the model
jsConfiguration.OwnsOne(s => s.CompleteName);
jsConfiguration.OwnsOne(s => s.HomeAddress);
jsConfiguration.OwnsOne(s => s.BillingAddress);
jsConfiguration.OwnsOne(s => s.EmAddress);
jsConfiguration.OwnsOne(s => s.PersonalPhoneNumber);
jsConfiguration.OwnsMany(a => a.CreditCards);
//jsConfiguration.HasMany<CreditCard>().WithOne(JobSeeker).OnDelete(DeleteBehavior.Restrict);
jsConfiguration.Property<DateTime>("CreatedDate");
jsConfiguration.Property<DateTime>("UpdatedDate");
}
}
class CreditCardTypeEntityTypeConfiguration : IEntityTypeConfiguration<CreditCard>
{
public void Configure(EntityTypeBuilder<CreditCard> ccConfiguration)
{
if (ccConfiguration == null)
{
throw new ArgumentNullException(nameof(ccConfiguration));
}
// Build the model
ccConfiguration.HasOne(o => o.CardType).WithMany().HasForeignKey("_creditCardTypeID");
ccConfiguration.Property<DateTime>("CreatedDate");
ccConfiguration.Property<DateTime>("UpdatedDate");
}
}
class CreditCardEntityTypeConfiguration : IEntityTypeConfiguration<CreditCardType>
{
public void Configure(EntityTypeBuilder<CreditCardType> cctConfiguration)
{
if (cctConfiguration == null)
{
throw new ArgumentNullException(nameof(cctConfiguration));
}
// Build the model
cctConfiguration.ToTable("CreditCardTypes");
cctConfiguration.HasKey(o => o.Id);
cctConfiguration.Property(o => o.Id)
.HasDefaultValue(1)
.ValueGeneratedNever()
.IsRequired();
cctConfiguration.Property(o => o.Name)
.HasMaxLength(200)
.IsRequired();
cctConfiguration.HasData(
new { Id = 1, Name = "Amex" },
new { Id = 2, Name = "Visa" },
new { Id = 3, Name = "MasterCard" });
}
}
My DB Context is:
public class JobSeekerContext : DbContext, IUnitOfWork
{
private static readonly Type[] EnumerationTypes = { typeof(CreditCardType) };
public const string DEFAULT_SCHEMA = "jobseeker";
private readonly ILoggerFactory MyConsoleLoggerFactory;
private readonly IMediator Mediator;
public DbSet<JobSeeker> JobSeekers { get; set; }
public DbSet<CreditCard> CreditCards { get; set; }
public DbSet<CreditCardType> CreditCardTypes { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
if (modelBuilder == null)
{
throw new ArgumentNullException(nameof(modelBuilder));
}
// Build the model
modelBuilder.ApplyConfiguration(new CreditCardTypeEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new CreditCardEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new JobSeekerEntityTypeConfiguration());
}
When I run the migration I get the following error: "The type 'CreditCard' cannot be marked as owned because a non-owned entity type with the same name already exists."
Where is CreditCard marked as non-owned?

Where is CreditCard marked as non-owned?
In JobSeekerContext here
public DbSet<CreditCard> CreditCards { get; set; }
and here
modelBuilder.ApplyConfiguration(new CreditCardTypeEntityTypeConfiguration());
and the whole (misleadingly named) CreditCardTypeEntityTypeConfiguration class since it is IEntityTypeConfiguration<CreditCard>.
Owned entity types are special entities which are configured, queried and updated only through owner.
Here is the excerpt from the By-design restrictions section of the current EF Core documentation:
You cannot create a DbSet<T> for an owned type.
You cannot call Entity<T>() with an owned type on ModelBuilder.
Note that applying IEnityTypeConfiguration<T> class is equivalent of calling Entity<T>() on ModelBuilder.
So you are breaking both aforementioned restrictions.
What you need to do is
Remove the DbSet<CreditCard> property from the context
Remove CreditCardTypeEntityTypeConfiguration class and corresponding ApplyConfiguration call
Move the CreditCard configuration code inside the owner JobSeeker configuration using the builder provided/returned by the OwnsMany method. e.g.
jsConfiguration.OwnsMany(a => a.CreditCards, ccConfiguration =>
{
// Build the model
ccConfiguration.HasOne(o => o.CardType).WithMany().HasForeignKey("_creditCardTypeID");
ccConfiguration.Property<DateTime>("CreatedDate");
ccConfiguration.Property<DateTime>("UpdatedDate");
});

Related

EF Core 3.1: Navigation property doesn't lazy load entities when calling the backing field first

I am using EF Core 3.1.7. The DbContext has the UseLazyLoadingProxies set. Fluent API mappings are being used to map entities to the database. I have an entity with a navigation property that uses a backing field. Loads and saves to the database seem to work fine except for an issue when accessing the backing field before I access the navigation property.
It seems that referenced entities don't lazy load when accessing the backing field. Is this a deficiency of the Castle.Proxy class or an incorrect configuration?
Compare the Student class implementation of IsRegisteredForACourse to the IsRegisteredForACourse2 for the behavior in question.
Database tables and relationships.
Student Entity
using System.Collections.Generic;
namespace EFCoreMappingTests
{
public class Student
{
public int Id { get; }
public string Name { get; }
private readonly List<Course> _courses;
public virtual IReadOnlyList<Course> Courses => _courses.AsReadOnly();
protected Student()
{
_courses = new List<Course>();
}
public Student(string name) : this()
{
Name = name;
}
public bool IsRegisteredForACourse()
{
return _courses.Count > 0;
}
public bool IsRegisteredForACourse2()
{
//Note the use of the property compare to the previous method using the backing field.
return Courses.Count > 0;
}
public void AddCourse(Course course)
{
_courses.Add(course);
}
}
}
Course Entity
namespace EFCoreMappingTests
{
public class Course
{
public int Id { get; }
public string Name { get; }
public virtual Student Student { get; }
protected Course()
{
}
public Course(string name) : this()
{
Name = name;
}
}
}
DbContext
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace EFCoreMappingTests
{
public sealed class Context : DbContext
{
private readonly string _connectionString;
private readonly bool _useConsoleLogger;
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
public Context(string connectionString, bool useConsoleLogger)
{
_connectionString = connectionString;
_useConsoleLogger = useConsoleLogger;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddFilter((category, level) =>
category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information)
.AddConsole();
});
optionsBuilder
.UseSqlServer(_connectionString)
.UseLazyLoadingProxies();
if (_useConsoleLogger)
{
optionsBuilder
.UseLoggerFactory(loggerFactory)
.EnableSensitiveDataLogging();
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>(x =>
{
x.ToTable("Student").HasKey(k => k.Id);
x.Property(p => p.Id).HasColumnName("Id");
x.Property(p => p.Name).HasColumnName("Name");
x.HasMany(p => p.Courses)
.WithOne(p => p.Student)
.OnDelete(DeleteBehavior.Cascade)
.Metadata.PrincipalToDependent.SetPropertyAccessMode(PropertyAccessMode.Field);
});
modelBuilder.Entity<Course>(x =>
{
x.ToTable("Course").HasKey(k => k.Id);
x.Property(p => p.Id).HasColumnName("Id");
x.Property(p => p.Name).HasColumnName("Name");
x.HasOne(p => p.Student).WithMany(p => p.Courses);
});
}
}
}
Test program which demos the issue.
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Linq;
namespace EFCoreMappingTests
{
class Program
{
static void Main(string[] args)
{
string connectionString = GetConnectionString();
using var context = new Context(connectionString, true);
var student2 = context.Students.FirstOrDefault(q => q.Id == 5);
Console.WriteLine(student2.IsRegisteredForACourse());
Console.WriteLine(student2.IsRegisteredForACourse2()); // The method uses the property which forces the lazy loading of the entities
Console.WriteLine(student2.IsRegisteredForACourse());
}
private static string GetConnectionString()
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
return configuration["ConnectionString"];
}
}
}
Console Output
False
True
True
When you declare a mapped property in an EF entity as virtual, EF generates a proxy which is capable of intercepting requests and assessing whether the data needs to be loaded. If you attempt to use a backing field before that virtual property is accessed, EF has no "signal" to lazy load the property.
As a general rule with entities you should always use the properties and avoid using/accessing backing fields. Auto-initialization can help:
public virtual IReadOnlyList<Course> Courses => new List<Course>().AsReadOnly();

Using DTO with OData in .NetCore 2.1

I am writing a test OData Rest API with an InMemoryDatabase.
I would like to use DTO(s) to hide the SQL model and adjust a few fields (geographic positions and so on).
However, when I use ProjectTo<...> method from AutoMapper, GET request to the API return an empty collection instead of the actual result list.
Do you have any idea about what I am doing wrong ?
Here is the controller :
namespace offers_api.Controllers
{
public class OffersController : ODataController
{
private readonly OfferContext _context;
private IMapper _mapper;
public OffersController(OfferContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
[EnableQuery]
public IActionResult Get()
{
IQueryable<Offer> res = _context.Offers.ProjectTo<Offer>(_mapper.ConfigurationProvider); // <-- works without ProjectTo !
return Ok(res);
}
}
}
The automapper declaration :
namespace offers_api.Entities
{
public class Mapping : Profile
{
public Mapping()
{
//CreateMap<CategoryEntity, string>().ConvertUsing(cat => cat.Name ?? string.Empty);
CreateMap<LocationEntity, Location>()
.ForMember(x => x.longitude, opt => opt.MapFrom(o => 0))
.ForMember(x => x.latitude, opt => opt.MapFrom(o => 0))
.ReverseMap();
CreateMap<OfferEntity, Offer>()
.ForMember(x => x.Category, opt => opt.MapFrom(o => o.Category.Name))
.ReverseMap()
.ForMember(x => x.Category, opt => opt.MapFrom(o => new CategoryEntity { Name = o.Category }));
CreateMap<OfferPictureEntity, OfferPicture>().ReverseMap();
CreateMap<UserEntity, User>().ReverseMap();
}
}
}
The EDM model :
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Offer>("Offers");
return builder.GetEdmModel();
}
I found the solution.
In fact, automapper loaded more data than OData's default behaviour.
The relation between an offer and it's author was described by a non nullable foreing key. I didn't insert any author in the DB, but OData tried to load a user and saw it was missing in the USER table, so it discarded the Offer result.
Solution : make the foreign key nullable.
namespace offers_api.Entities
{
public class OfferEntity
{
[Key]
public long Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public long AuthorId { get; set; } // <== Bug here : add long? to resolve it...
public virtual UserEntity Author { get; set; }
}
}

Entity Framework: Cannot be configured on 'xxx' class because it is a derived type

I got this error when try add "AppUserRoles" table by myself to access UserRoles from User, for example:
from u in _userManager.Users.Where(x => x.UserRoles "contain" "some codition here")
And I got this error:
A key cannot be configured on 'AppUserRoles' because it is a derived type. The key must be configured on the root type 'IdentityUserRole'. If you did not intend for 'IdentityUserRole' to be included in the model, ensure that it is not included in a DbSet property on your context, referenced in a configuration call to ModelBuilder, or referenced from a navigation property on a type that is included in the model.
This is my previous code, It is run ok:
builder.Entity<IdentityUserRole<Guid>>().ToTable("AppUserRoles")
.HasKey(x => new { x.RoleId, x.UserId });
--->And I change to this:
My user class
[Table("AppUsers")]
public class AppUser : IdentityUser<Guid>, IDateTracking, ISwitchable
{
public virtual ICollection<AppUserRoles> UserRoles { get; set; }
}
My role class
[Table("AppRoles")]
public class AppRole : IdentityRole<Guid>
{
public virtual ICollection<AppUserRoles> UserRoles { get; set; }
}
My appUserRole class:
[Table("AppUserRoles")]
public class AppUserRoles : IdentityUserRole<Guid>
{
public virtual AppUser User { get; set; }
public virtual AppRole Role { get; set; }
}
My DbContextClass
public class AppDbContext : IdentityDbContext<AppUser, AppRole, Guid>
{
public AppDbContext(DbContextOptions options) : base(options)
{
}
public DbSet<AppUser> AppUsers { get; set; }
public DbSet<AppRole> AppRoles { get; set; }
public DbSet<AppUserRoles> AppUserRoles { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
#region Identity Config
builder.Entity<IdentityUserClaim<Guid>>().ToTable("AppUserClaims").HasKey(x => x.Id);
builder.Entity<IdentityUserLogin<Guid>>().ToTable("AppUserLogins").HasKey(x => x.UserId);
builder.Entity<AppUserRoles>(userRole =>
{
userRole.HasKey(ur => new { ur.UserId, ur.RoleId });
userRole.HasOne(ur => ur.Role)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
userRole.HasOne(ur => ur.User)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
builder.Entity<IdentityUserToken<Guid>>().ToTable("AppUserTokens")
.HasKey(x => new { x.UserId });
#endregion Identity Config
}
public override int SaveChanges()
{
var modified = ChangeTracker.Entries().Where(e => e.State == EntityState.Modified || e.State == EntityState.Added);
foreach (EntityEntry item in modified)
{
var changedOrAddedItem = item.Entity as IDateTracking;
if (changedOrAddedItem != null)
{
if (item.State == EntityState.Added)
{
changedOrAddedItem.DateCreated = DateTime.Now;
}
changedOrAddedItem.DateModified = DateTime.Now;
}
}
return base.SaveChanges();
}
}
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] args)
{
IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json").Build();
var builder = new DbContextOptionsBuilder<AppDbContext>();
var connectionString = configuration.GetConnectionString("DefaultConnection");
builder.UseSqlServer(connectionString);
return new AppDbContext(builder.Options);
}
}
This is my startup file:
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
o => o.MigrationsAssembly("YayoiApp.Data.EF")),
ServiceLifetime.Scoped);
Please give me some advise, I already have researched to much, but not found a solution. Thanks.
For custom IdentityUserRole<Guid>, you need to change your DbContext like
public class ApplicationDbContext : IdentityDbContext<AppUser
, AppRole
, Guid
, IdentityUserClaim<Guid>
, AppUserRoles
, IdentityUserLogin<Guid>
, IdentityRoleClaim<Guid>
, IdentityUserToken<Guid>>
{
Then register it in Startup.cs
services.AddIdentity<AppUser, AppRole>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
UseCase:
public class HomeController : Controller
{
private readonly UserManager<AppUser> _userManager;
private readonly RoleManager<AppRole> _roleManager;
public HomeController(UserManager<AppUser> userManager
, RoleManager<AppRole> roleManager)
{
_userManager = userManager;
_roleManager = roleManager;
}
public async Task<IActionResult> Index()
{
var userName = "Tom";
var passWord = "1qaz#WSX";
var appUser = new AppUser
{
UserName = userName
};
await _userManager.CreateAsync(appUser, passWord);
var roleName = "Admin";
await _roleManager.CreateAsync(new AppRole { Name = roleName });
await _userManager.AddToRoleAsync(appUser, roleName);
var roles = appUser.UserRoles;
return View();
}

ASP.NET core indentity error: Entity type 'AppUserLogin' is defined with a single key property, but 2 values were passed to the 'DbSet.Find' method

I override Identity classes:
[Table("Roles")]
public partial class AppRole : IdentityRole<int, AppUserRole, AppRoleClaim>
{
}
[Table("RoleClaims")]
public partial class AppRoleClaim : IdentityRoleClaim<int>
{
}
[Table("Users")]
public partial class AppUser : IdentityUser<int, AppUserClaim, AppUserRole, AppUserLogin>
{
}
[Table("UserClaims")]
public partial class AppUserClaim : IdentityUserClaim<int>
{
}
[Table("UserLogins")]
public partial class AppUserLogin : IdentityUserLogin<int>
{
[Key]
[Required]
public int Id { get; set; }
}
[Table("UserRoles")]
public partial class AppUserRole : IdentityUserRole<int>
{
}
[Table("UserTokens")]
public partial class AppUserToken : IdentityUserToken<int>
{
[Key]
[Required]
public int Id { get; set; }
}
public class AppRoleManager : RoleManager<AppRole>
{
public AppRoleManager(IRoleStore<AppRole> store, IEnumerable<IRoleValidator<AppRole>> roleValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, ILogger<RoleManager<AppRole>> logger, IHttpContextAccessor contextAccessor) : base(store, roleValidators, keyNormalizer, errors, logger, contextAccessor)
{
}
}
public class AppRoleStore : RoleStore<AppRole, AppDbContext, int, AppUserRole, AppRoleClaim>
{
public AppRoleStore(AppDbContext context, IdentityErrorDescriber describer = null) : base(context, describer)
{
}
protected override AppRoleClaim CreateRoleClaim(AppRole role, Claim claim)
{
return new AppRoleClaim(role, claim);
}
}
public class AppSignInManager : SignInManager<AppUser>
{
public AppSignInManager(UserManager<AppUser> userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory<AppUser> claimsFactory, IOptions<IdentityOptions> optionsAccessor, ILogger<SignInManager<AppUser>> logger) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger)
{
}
}
public class AppUserManager : UserManager<AppUser>
{
public AppUserManager(IUserStore<AppUser> store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<AppUser> passwordHasher, IEnumerable<IUserValidator<AppUser>> userValidators, IEnumerable<IPasswordValidator<AppUser>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<AppUser>> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
var LoginStore = Store;
}
}
public class AppUserStore : UserStore<AppUser, AppRole, AppDbContext, int, AppUserClaim, AppUserRole, AppUserLogin, AppUserToken, AppRoleClaim>
{
public AppUserStore(AppDbContext context, IdentityErrorDescriber describer = null) : base(context, describer)
{
}
protected override AppUserClaim CreateUserClaim(AppUser user, Claim claim)
{
return new AppUserClaim(user, claim);
}
protected override AppUserLogin CreateUserLogin(AppUser user, UserLoginInfo login)
{
return new AppUserLogin(user, login);
}
protected override AppUserRole CreateUserRole(AppUser user, AppRole role)
{
return new AppUserRole(user, role);
}
protected override AppUserToken CreateUserToken(AppUser user, string loginProvider, string name, string value)
{
return new AppUserToken(user, loginProvider, name, value);
}
}
and I want to use external login for Google, but when I call await UserManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); I get error:
Entity type 'AppUserLogin' is defined with a single key property, but
2 values were passed to the 'DbSet.Find' method.
I am following tutorials openiddict and ASP.NET Core and Angular 2, but I am pretty sure this error does not have anything with this 2 tutorials. I think my code is not working, because I change TKey for UserLogins table: public partial class AppUserLogin : IdentityUserLogin<int>. Even searching for is defined with a single key property results nothing.
Solution is in DbContext.OnModelCreating method.
I change from:
modelBuilder.Entity<AppUserLogin>(entity =>
{
entity
.HasKey(u => u.Id);
entity.Property(p => p.Id)
.ValueGeneratedOnAdd();
});
to:
modelBuilder.Entity<AppUserLogin>(entity =>
{
entity
.HasKey(u => u.Id);
entity.Property(p => p.Id)
.ValueGeneratedOnAdd();
entity
.HasKey(u => new { u.LoginProvider, u.ProviderKey });
});
and it works.

Updating and Deleting Associated Records in a One-To-Many Relationship using Entity Framework

Using EF 4.1 Code First, I have a Member entity and it in turn has two "one-to-many" relationships for a HomeAddress and WorkAddress. It also has a boolean property to state whether or not to use either of these addresses.
I have two issues that I can't figure out:
Whenever I update a member's address, a new record is added to the MemberAddresses table (with a new ID value) and the existing record is not deleted. Though it looks fine from the front-end perspective as the HomeAddressId and WorkAddressId in the parent Members table is updated with the new record, the old records are kept in the table (orhpaned). I don't want it to add a new address record when the address is updated. I only want it to update the existing record. If it has to add a new one, then I at least want it to clear out the old one.
There are times that I want to delete the address record from the table. For example, if the member previously had an associated HomeAddress and later the DontUseHomeAddress is set to true, I want the address to be deleted from the table. So far, I have tried setting it to null, but that just prevents any updates. It doesn't delete it.
I'm sure there just some code piece I'm missing, so I'm including all the relevant code below that I think might affect this.
public abstract class Entity
{
public int Id { get; set; }
}
public class Member : Entity
{
public string Name { get; set; }
public bool DontUseHomeAddress { get; set; }
public virtual MemberAddress HomeAddress { get; set; }
public bool DontUseWorkAddress { get; set; }
public virtual MemberAddress WorkAddress { get; set; }
//... other properties here ...
}
public class MemberMap : EntityTypeConfiguration<Member>
{
public MemberMap()
{
ToTable("Members");
Property(m => m.Name).IsRequired().HasMaxLength(50);
//TODO: Somehow this is creating new records in the MemberAddress table instead of updating existing ones
HasOptional(m => m.HomeAddress).WithMany().Map(a => a.MapKey("HomeAddressId"));
HasOptional(m => m.WorkAddress).WithMany().Map(a => a.MapKey("WorkAddressId"));
}
}
public class MemberAddressMap : EntityTypeConfiguration<MemberAddress>
{
public MemberAddressMap()
{
ToTable("MemberAddresses");
Property(x => x.StreetAddress).IsRequired().HasMaxLength(255);
Property(x => x.City).IsRequired().HasMaxLength(50);
Property(x => x.State).IsRequired().HasMaxLength(2);
Property(x => x.ZipCode).IsRequired().HasMaxLength(5);
}
}
Here is the InsertOrUpdate method from my repository class that my controller calls:
public class Repository<TEntity> : IRepository<TEntity> where TEntity : Entity
{
private readonly EfDbContext _context;
private readonly DbSet<TEntity> _dbSet;
public Repository(EfDbContext context)
{
_context = context;
_dbSet = _context.Set<TEntity>();
}
public bool InsertOrUpdate(TEntity entity)
{
if(entity.Id == 0)
{
_dbSet.Add(entity);
}
else
{
_context.Entry(entity).State = EntityState.Modified;
}
_context.SaveChanges();
return true;
}
//... Other repository methods here ...
}
EDIT: Adding in code for UnitOfWork and MemberServices
public class MemberServices : IMemberServices
{
private readonly IUnitOfWork _unitOfWork;
private readonly IRepository _memberRepository;
public MemberServices(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
_memberRepository = unitOfWork.RepositoryFor<Member>();
}
public Member Find(int id)
{
return _memberRepository.FindById(id);
}
public bool InsertOrUpdate(Member member)
{
// if(member.HomeAddress != null)
// _unitOfWork.SetContextState(member.HomeAddress, EntityState.Modified);
//
// if(member.WorkAddress != null)
// _unitOfWork.SetContextState(member.WorkAddress, EntityState.Modified);
//
// if(member.DontUseHomeAddress)
// {
// //TODO: This is an attempted hack... fix it by moving somewhere (possibly to repository)
// var context = new EfDbContext();
// context.Set<MemberAddress>().Remove(member.HomeAddress);
// context.SaveChanges();
// }
_memberRepository.InsertOrUpdate(member);
return true;
}
}
public class UnitOfWork : IUnitOfWork
{
private readonly EfDbContext _context;
public UnitOfWork()
{
_context = new EfDbContext();
}
public IRepository<T> RepositoryFor<T>() where T : Entity
{
return new Repository<T>(_context);
}
public void Attach(Entity entity)
{
_context.Entry(entity).State = EntityState.Unchanged;
}
public void SetContextState(Entity entity, EntityState state)
{
_context.Entry(entity).State = state;
}
public void Save()
{
_context.SaveChanges();
}
}
Setting the state _context.Entry(entity).State = EntityState.Modified; doesn't affect the state of related entities. If you want to take care of changes of your related entities you must set their state to Modified as well:
if (member.HomeAddress != null)
_context.Entry(member.HomeAddress).State = EntityState.Modified;
if (member.WorkAddress != null)
_context.Entry(member.WorkAddress).State = EntityState.Modified;
_context.Entry(member).State = EntityState.Modified;
This is not generic anymore.
To delete an entity you have to call the appropriate method to delete an entity; setting the navigation property to null is not enough:
_context.MemberAddresses.Remove(member.HomeAddress);