I'm using ASP.NET Core 2.2 and EF. And I have these entities:
public class ActionCategory
{
public short Id { get; set; }
[Required]
public string Name { get; set; }
public IEnumerable<Action> Actions { get; set; }
}
public class Action
{
public string Id { get; set; }
[Required]
public string Name { get; set; }
public short CategoryId { get; set; }
[ForeignKey("CategoryId")]
public ActionCategory Category { get; set; }
}
I want to automatically delete ActionCategory if all Actions were deleted. I understand that I can implement this logic myself but I wonder if there is some EF feature allows to do that. And if there is no such feature could I write a trigger for MySQL database? Wouldn't it be a bad way? I mean split my business logic to controllers and database? Or vice versa it will be a good practice to make this logic on database level?
Related
I'm doing some practice with Entity Framework and I want to create a web-api backend, which is able to manage requests by interacting with a MySql database and by responding with JSON strings in the message body of the replies. I'm quite new to it and I'm trying to learn it solving problem by problem in the project.
An example of the working code at the moment is:
public string getFilmById(int id)
{
return JsonSerializer.Serialize(ctx.Films.ToList().Where(filmToGet => filmToGet.Id == id));
}
Which generates the string:
[{"Id":7,"Title":"Dune","Genre":"Sci-fi","Duration":155,"Direction":[],"Interpretation":[]}]
Now, what I want to do is to include in the string the directors and the actors of the film. I've created all the models of the backend with a database-first approach, by importing and converting MySql tables into classes and by generating the dbcontext class automatically. This implies that I also have the Direction class to link Film and Director, because the relationship is Many to Many (the same is for Interpretation and Actor). Here the code:
public partial class Film
{
public Film()
{
Direction = new HashSet<Direction>();
Interpretation = new HashSet<Interpretation>();
}
public int Id { get; set; }
public string Title { get; set; }
public string Genre { get; set; }
public int? Duration { get; set; }
public virtual ICollection<Direction> Directions { get; set; }
public virtual ICollection<Interpretation> Interpretations { get; set; }
}
public partial class Direction
{
public int Id { get; set; }
public int? IdDirector { get; set; }
public int? IdFilm { get; set; }
public virtual Film IdFilmNavigation { get; set; }
public virtual Director IdDirectorNavigation { get; set; }
}
public partial class Director
{
public Director()
{
Directions = new HashSet<Direction>();
}
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public virtual ICollection<Direction> Directions { get; set; }
}
In the end: how can I include Directors and Actors in the Film instance and make them appear in that json string? Technically this is a simple JOIN after all, but I don't really know how to do it using these classes.
Thank you for the help!
Here's the repo with all the code if can help:
https://github.com/marco-savino/film_archive_project.git
I am using the Sakila Sample Database from MySql on a MySql server. The Diagram looks as follows.
The important tables are the store, inventory and film tables. The is a many-to-many relationship between the tables and the linker table is the inventory table.
I scaffolded this Database in a new dotnetcore project using EFCore 2.
I am trying to get a list of stores and their list of films.
The Entities are defined as follows:
Store
public class Store
{
public Store()
{
Customer = new HashSet<Customer>();
Inventory = new HashSet<Inventory>();
Staff = new HashSet<Staff>();
}
public byte StoreId { get; set; }
public byte ManagerStaffId { get; set; }
public short AddressId { get; set; }
public DateTimeOffset LastUpdate { get; set; }
public Address Address { get; set; }
public Staff ManagerStaff { get; set; }
public ICollection<Customer> Customer { get; set; }
public ICollection<Inventory> Inventory { get; set; }
public ICollection<Staff> Staff { get; set; }
}
Inventory
public partial class Inventory
{
public Inventory()
{
Rental = new HashSet<Rental>();
}
public int InventoryId { get; set; }
public short FilmId { get; set; }
public byte StoreId { get; set; }
public DateTimeOffset LastUpdate { get; set; }
public Film Film { get; set; }
public Store Store { get; set; }
public ICollection<Rental> Rental { get; set; }
}
Film
public partial class Film
{
public Film()
{
FilmActor = new HashSet<FilmActor>();
FilmCategory = new HashSet<FilmCategory>();
Inventory = new HashSet<Inventory>();
}
public short FilmId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public short? ReleaseYear { get; set; }
public byte LanguageId { get; set; }
public byte? OriginalLanguageId { get; set; }
public byte RentalDuration { get; set; }
public decimal RentalRate { get; set; }
public short? Length { get; set; }
public decimal ReplacementCost { get; set; }
public string Rating { get; set; }
public string SpecialFeatures { get; set; }
public DateTimeOffset LastUpdate { get; set; }
public Language Language { get; set;
public Language OriginalLanguage { get; set; }
public ICollection<FilmActor> FilmActor { get; set; }
public ICollection<FilmCategory> FilmCategory { get; set; }
public ICollection<Inventory> Inventory { get; set; }
}
My context looks as follows:
modelBuilder.Entity<Inventory>(entity =>
{
entity.ToTable("inventory", "sakila");
entity.HasIndex(e => e.FilmId)
.HasName("idx_fk_film_id");
entity.HasIndex(e => new { e.StoreId, e.FilmId })
.HasName("idx_store_id_film_id");
And lastly the repo looks as follows:
public IEnumerable<Store> GetStores()
{
return _context.Store.
Include(a => a.Inventory).
ToList();
}
Problem:
When I call this method from a Controller to get the list of stores I don´t get any json response on Postman. Yet if I debug into the list that is returned from the Controller I find the list of stores.
The problem is that the list contains:
store->inventory->film->store->inventory->film->store... Etc. Creating a circular dependency that fills up the allowed Process memory of the request.
Possible Solutions:
I think it has to do with the fact that on the Context both the Foreign Keys are defined as HasIndex instead of HasKey
entity.HasIndex(e => new { e.StoreId, e.FilmId })
.HasName("idx_store_id_film_id");
When I define it as HasKey then I get an Error:
'The relationship from 'Rental.Inventory' to 'Inventory.Rental' with
foreign key properties {'InventoryId' : int} cannot target the primary
key {'StoreId' : byte, 'FilmId' : short} because it is not compatible.
Configure a principal key or a set of compatible foreign key
properties for this relationship.'
To answer #hamzas comment, I did find a solution to this problem. I used EFCore to build the entities and the DBContext through scaffolding (DB First). As a best practice you should be using Models (Dtos) to represent the Data for the client. EFCore is very helpful in giving us the flexibility to access this M to N relationship however we want. This gives us the flexibility to represent this Data to the client however we want.
Whatever your use case might be. You have to convert the M to N relationship into an 1 to N model.
Use Case #1: You want to show all the movies for a specific store.
Solution
Step #1: You create a StoreDto (Model)
public class StoreDto
{
int StoreId { get; set; }
ICollection<FilmDto> Films { get; set; }
= new List<FilmDto> ();
}
Step #2: Create a FilmDto
public class FilmDto
{
int FilmId { get; set; }
int StoreId { get; set; }
string FilmName { get; set; }
}
Step #3: You provide a Mapping with auto mapper
public class MappingProfiles : Profile
{
public MappingProfiles()
{
CreateMap<Store, StoreDto>();
CreateMap<Film, FilmDto>();
}
}
Step #4: Query the data correctly, Unfortunately I don´t have this example anymore to test this code, so here is where you´ll have to experiment a bit
public Store GetFilmsForStore(byte StoreId)
{
return _context.Store.
Include(a => a.Inventory).
ThenInclude(i => i.Film)
ToList();
}
On the "Include" part you want to only get the Inventory entries where StoreId == Inverntory.StoreId and then Include the Films Object from the resulting list.
I hope you get the jist of it. You want to break up your m to n relationships and make them seem like 1 to m for your clients.
I have a Mysql Database and want to create a scaffold to try a data-first .net project. I got the scaffold to work, which was great, but it is creating these false Collections on my model objects for related tables.
For my coin object below, I only have the 5 fields on the top, but it is creating the collections to any of the tables where Coin is a foreign key. This is not really making any sense to me. I will never populate this data and cannot see any settings to stop this from happening.
public partial class Coin
{
public Coin()
{
LedgerTransactions = new HashSet<LedgerTransactions>();
Price = new HashSet<Price>();
TradeFeeCoin = new HashSet<Trade>();
TradeForCoin = new HashSet<Trade>();
TradeTradeCoin = new HashSet<Trade>();
}
[Column(TypeName = "int(11)")]
public int Id { get; set; }
[Required]
[StringLength(50)]
public string Name { get; set; }
[Required]
[StringLength(5)]
public string Symbol { get; set; }
public byte? SortOrd { get; set; }
[Column(TypeName = "bit(1)")]
public bool? IsBaseCurrency { get; set; }
[InverseProperty("Coin")]
public ICollection<LedgerTransactions> LedgerTransactions { get; set; }
[InverseProperty("Coin")]
public ICollection<Price> Price { get; set; }
[InverseProperty("FeeCoin")]
public ICollection<Trade> TradeFeeCoin { get; set; }
[InverseProperty("ForCoin")]
public ICollection<Trade> TradeForCoin { get; set; }
[InverseProperty("TradeCoin")]
public ICollection<Trade> TradeTradeCoin { get; set; }
}
I created this table from a Code-First approach, and that model only had the 5 fields in it that I would expect. Am I doing something wrong?
Here is my Master Entity who will contains a list of Language
public partial class WebSite
{
public WebSite()
{
this.WebSiteLanguages = new HashSet<WebSiteLanguage>();
}
public int Id { get; set; }
public Nullable<int> WLUserID { get; set; }
public string DomainName { get; set; }
public Nullable<bool> IsActive { get; set; }
//[Required]
public virtual ICollection<WebSiteLanguage> WebSiteLanguages { get; set; }
}
My WebSiteLanguage Child class is
public partial class WebSiteLanguage
{
public int Id { get; set; }
public string LanguageName { get; set; }
public Nullable<int> WebSiteID { get; set; }
public bool IsDefault { get; set; }
public virtual WebSite WebSite { get; set; }
}
In my View, I can Add many language as I want within an ajax call.
My Question is :
Is it possible to make the
public virtual ICollection WebSiteLanguages { get;
set; }
Required. The Website Entity is not valid if there is no WebSiteLanguage created.
Thanks a lot.
As per post http://blogs.msdn.com/b/adonet/archive/2011/05/27/ef-4-1-validation.aspx navigation properties are excluded from facet validation "as you could set the associated FK value and the navigation property would be set on SaveChanges()". To validate that a navigation property is not null you can:
create a custom attribute that validates it (be it on the type or on the property)
implement IValidatableObject interface that does the above
override DbContext.ValidateEntity protected method so that it validates that the property is not null and if this is the case calls base.ValidateEntity() to validate other properties (see this for more details: http://blogs.msdn.com/b/adonet/archive/2010/12/15/ef-feature-ctp5-validation.aspx)
The 3rd solution seems to be the cleanest.
I have a class department inheriting from activeentity
public class ActiveEntity : Entity, IActive
{
public ActiveEntity()
{
IsActive = true;
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid Id { get; set; }
public bool IsActive { get; set; }
[Timestamp, ScaffoldColumn(false), DatabaseGenerated(System.ComponentModel.DataAnnotations.DatabaseGeneratedOption.Computed)]
public Byte[] Timestamp { get; set; }
[ScaffoldColumn(false)]
public string CreationUserId { get; set; }
[ScaffoldColumn(false)]
public string LastModifiedUserId { get; set; }
}
public class Department:ActiveEntity
{
public Department()
{
this.Address = new DepartmentAddress();
}
[StringLength(9),MinLength(9),MaxLength(9)]
public string Name { get; set; }
public Guid ManagerId { get; set; }
[UIHint("AjaxDropdown")]
public User Manager { get; set; }
public Guid? AddressId { get; set; }
public DepartmentAddress Address { get; set; }
public ICollection<OverheadRate> OverheadRates { get; set; }
}
I am just using annotations no Fluent API. The data saves to the data Sql Server 2008 just fine however the address object never gets instantiated, even though I have the context use the include
return c.Set<Department>().Include(d => d.Address).Include(d => d.Manager).Where(predicate);
The data is returned I run sql profiler and then run the query it returns the correct data.
Any thoughts or suggestions?
Remove instantiating the address (this.Address = new DepartmentAddress();) in the Department constructor. Instantiating navigation references in the default constructor is evil and has nasty side effects like these:
What would cause the Entity Framework to save an unloaded (but lazy loadable) reference over existing data?
EF 4.1 Code First: Why is EF not setting this navigation property?