TL;DR;
NHibernate reverse relationship is working on Azure-SQL and MSSQL2012 but not with SQLite
Description:
I am currently Unittesting my Asp.Net MVC App and set up my Unittest with FluentMigrator on SQLite.
After creating the Database I set up some base entries I need.
One of those is a Product.
A Product has many ProductSuppliers and a ProductSupplier has many ProductSupplierPrices
public class Product
{
public virtual long Id { get; set; }
public virtual string Number { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
//more properties
public virtual IList<ProductSupplier> Suppliers { get; set; }
//more properties
}
public class ProductSupplier
{
public virtual long Id { get; set; }
public virtual Product Product { get; set; }
public virtual Supplier Supplier { get; set; }
public virtual IList<ProductSupplierPrice> Prices { get; set; }
}
public class ProductSupplierPrice : IHaveId
{
public virtual long Id { get; set; }
public virtual ProductSupplier ProductSupplier { get; set; }
public virtual decimal FromAmount { get; set; }
public virtual decimal Price { get; set; }
}
Setup:
Create Supplier
Create Product
Create ProductSupplier
Create ProductSupplierPrice
Test:
Product product = this.session.Load<Product>((long)1);
ProductSupplier productSupplier = product.Suppliers.First(); //<-- Suppliers are null; therefore throws an exception
If I load them seperately to check the relationships:
productSupplierPrice.ProductSupplier <--- Correct Supplier
productSupplier.Prices <-- Null
productSupplier.Product <-- Product with Id 1
product.Suppliers <-- Null
So to me it seems, that the many-to-one direction works correctely, but the one-to-many (reverse relation) is not working.
The Problem exists only in my Unittest (SQLite) the App itself runs on Azure-SQL and is working fine.
EDIT:
Mappings with FluentnHibernate
public class ProductMap : ClassMap<Product>
{
public ProductMap()
{
Id(x => x.Id);
HasMany(x => x.Suppliers).Inverse().Cascade.DeleteOrphan().BatchSize(20);
//many more mappings
}
}
public ProductSupplierMap()
{
Id(x => x.Id);
References(x => x.Product);
References(x => x.Supplier);
Map(x => x.IsMainSupplier);
Map(x => x.SupplierProductNumber);
Map(x => x.CopperSurcharge);
HasMany(x => x.Prices).Inverse().Cascade.DeleteOrphan().BatchSize(20);
}
public ProductSupplierPriceMap()
{
Id(x => x.Id);
References(x => x.ProductSupplier);
Map(x => x.FromAmount);
Map(x => x.Price);
}
Edit2 - Creating the DB-Entries:
Product product = new Product()
{
Type = ProductType.Purchase,
Dispatcher = session.Load<Employee>(employeeId),
Number = "100.10-1000",
Name = "Testproduct",
//Lots of Properties
Suppliers = new List<ProductSupplier>()
};
session.SaveOrUpdate(product);
ProductSupplier productSupplier = new ProductSupplier()
{
Product = product,
Supplier = session.Load<Supplier>((long)1),
IsMainSupplier = true,
SupplierProductNumber = "Artikel123456",
CopperSurcharge = CopperSurchargeType.DEL700,
Prices = new List<ProductSupplierPrice>()
};
session.Save(productSupplier);
ProductSupplierPrice productSupplierPrice = new ProductSupplierPrice()
{
ProductSupplier = productSupplier,
FromAmount = 1,
Price = 5
};
session.Save(productSupplierPrice);
EDIT 3.1:
public static ISession InitializeDatabase()
{
NHibernateSessionHolder.CreateSessionFactory();
session = NHibernateSessionHolder.OpenSession();
CreateBaseEntries(); //Creates Employees, Supplier, Customer etc
return session;
}
Based on the Ayende's article you need to clear the session between insert/update and querying:
session.Clear();
Seems to be a session management, I'm not sure why the session should be clean, but the session is providing your original instance (the same you provided for saving, stored on the session cache) instead a proxy for lazy-loading.
private long CreatePurchaseOrder()
{
session.Clear();
var product = this.session.Load<Product>((long)1);
var productSupplier = product.Suppliers.First();
var productSupplierPrice = productSupplier.Prices.First();
return 0;
}
Sorry for late reply
In your unit test, you are using same session for creating and fetching entities. This is not right as subsequent fetch returns entities from first level cache which do not have their graph set up properly.
So....either use different sessions OR as a quick fix, I have added "session.Clear()" in the method "InitializeDatabase()" of "DatabaseSetUpHelper". Clearing the session clears first level cache and force NH to fetch data from DB again and the resulting entities have their graph set up properly.
public static ISession InitializeDatabase()
{
NHibernateSessionHolder.CreateSessionFactory();
session = NHibernateSessionHolder.OpenSession();
CreateBaseEntries();
session.Clear(); // notice this!!! this clears first level cache of session, thus forcing fetching of data from DB
return session;
}
Note: My quick-fix is not final solution, it is there just show how session behaves. In proper solution, you must use different sessions.
Related
I am developing a Web API using Core 6.0 with localization. Localization should be supported for both static (e.g., basic strings like greeting) and dynamic content (e.g., Values of the Product Instance).
I have implemented the localization for static content using JsonStringLocalizerFactory as discussed in this article - https://christian-schou.dk/how-to-add-localization-in-asp-net-core-web-api/.
public class LocalizerController : ControllerBase
{
private readonly IStringLocalizer<LocalizerController> _stringLocalizer;
public LocalizerController(IStringLocalizer<LocalizerController> stringLocalizer)
{
_stringLocalizer = stringLocalizer;
}
[HttpGet]
public IActionResult Get()
{
var message = _stringLocalizer["hi"].ToString();
return Ok(message);
}
[HttpGet("{name}")]
public IActionResult Get(string name)
{
var message = string.Format(_stringLocalizer["welcome"], name);
return Ok(message);
}
[HttpGet("all")]
public IActionResult GetAll()
{
var message = _stringLocalizer.GetAllStrings();
return Ok(message);
}
}
Next, I would like to implement localization for dynamic content (e.g., Details of the Product which will be sent to the WEB API and stored in the postgresql database table).
A possible approach is to duplicate the postgresql database table for each language (English and French). Could there be a better approach to avoid duplicate data and additional manual work?
You can create language table for each multi-language entity.
Langugage model;
public class Language
{
public int Id { get; set; }
public string Name { get; set; }
public string IsoCode { get; set; }
}
Static language list;
public class Constant
{
public static List<Language> Languages { get; set; } = new()
{
new Language
{
Id = 1,
Name = "English(United States)",
IsoCode = "en-US"
},
new Language
{
Id = 2,
Name = "Turkish",
IsoCode = "tr-TR"
}
};
}
Entities;
public class Product
{
public int Id { get; set; }
public decimal Price { get; set; }
public virtual ICollection<ProductLang> ProductLangs { get; set; }
}
public class ProductLang
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
[ForeignKey("Products")]
public Guid ProductId { get; set; }
public virtual Product Product { get; set; }
public int LanguageId { get; set; }
}
You can change the LanguageId property name. If you want to store languages in database, you can create a Languages table and create a relationship with that table from entity language tables. This can reduce duplication.
After include the language table to the entity, you can write an extension method to easily get the requested language data.
public static string GetLang<TEntity>(this IEnumerable<TEntity> langs, Expression<Func<TEntity, string>> propertyExpression, int defaultLangId)
{
var languageIdPropName = nameof(ProductLang.LanguageId);
var requestedLangId = GetCurrentOrDefaultLanguageId(defaultLangId);
if (langs.IsNullOrEmpty())
return string.Empty;
var propName = GetPropertyName(propertyExpression);
TEntity requestedLang;
if (requestedLangId != defaultLangId)
requestedLang = langs.FirstOrDefault(lang => (int)lang.GetType()
.GetProperty(languageIdPropName)
.GetValue(lang) == requestedLangId)
?? langs.FirstOrDefault(lang => (int)lang.GetType()
.GetProperty(languageIdPropName)
.GetValue(lang) == defaultLangId);
else requestedLang = langs.FirstOrDefault(lang => (int)lang.GetType().GetProperty(languageIdPropName).GetValue(lang) == defaultLangId);
requestedLang ??= langs.FirstOrDefault();
return requestedLang.GetType().GetProperty(propName).GetValue(requestedLang, null)?.ToString();
static int GetCurrentOrDefaultLanguageId(int defaultLanguageId)
{
var culture = CultureInfo.CurrentCulture;
var currentLanguage = Constant.Languages.FirstOrDefault(i => i.IsoCode == culture.Name);
if (currentLanguage != null)
return currentLanguage.Id;
else
return defaultLanguageId;
}
static string GetPropertyName<T, TPropertyType>(Expression<Func<T, TPropertyType>> expression)
{
if (expression.Body is MemberExpression tempExpression)
{
return tempExpression.Member.Name;
}
else
{
var op = ((UnaryExpression)expression.Body).Operand;
return ((MemberExpression)op).Member.Name;
}
}
}
This extension method checks for 3 conditions;
If there is data in the requsted language, it returns this data,
If there is no data in the requsted language, it checks if there is data in the default language. If the data is available in the default language, it will return the data,
Returns the first available language data if there is no data in the default language
Usage;
var defaultLangId = 1;
Product someProduct = await _dbContext.Set<Product>().Include(i => i.ProductLangs).FirstOrDefaultAsync();
var productName = someProduct.ProductLangs.GetLang(i => i.Name, defaultLangId);
It is up to you to modify this extension method according to your own situation. I gave you an example scenario where languages are kept in a static list.
I'm trying to add a view as a Navigation Property of an entity.
public class Schedule
{
public int Id { get; set; }
public decimal ScheduledQuantity { get; set; }
public ScheduleDetails ScheduleDetails { get; set; }
}
public class ScheduleDetails
{
public int ScheduleId { get; set; }
public decimal BadQuantity { get; set; }
public Schedule Schedule { get; set; }
}
with mappings:
public class ScheduleDetailMap : IEntityTypeConfiguration<ScheduleDetails>
{
public void Configure(EntityTypeBuilder<ScheduleDetails> builder)
{
builder.ToView("vwScheduleDetails", "ShopOrders");
builder.HasKey(t => t.ScheduleId);
builder.HasOne(p => p.Schedule).WithOne(s => s.ScheduleDetails);
}
}
public class ScheduleMap : IEntityTypeConfiguration<Schedule>
{
public void Configure(EntityTypeBuilder<Schedule> builder)
{
builder.ToTable("Schedules");
builder.HasKey(t => t.Id);
builder.Property(t => t.Id).UseIdentityColumn();
}
}
when I query it works fine. However if I add a new Schedule record.
var schedule = new Schedule
{
ScheduledQuantity = 100,
ScheduleDetails = new ScheduleDetails()
};
context.Schedules.Add(schedule);
context.SaveChanges();
I get an exception saying " The entity type 'ScheduleDetails' is not mapped to a table, therefore the entities cannot be persisted to the database. Use 'ToTable' in 'OnModelCreating' to map it."
Is there anyway to get EF to ignore saving this 'entity'?
This is kind of an old question, but for anyone having similar issues - in my case the problem lied in navigation properties in my view. I had some leftover properties in view's class, because its code was copied from other entity. By removing those properties, the error was gone.
This doesn't really help if you want to use navigation properties in your code, but it may help someone to continue their search.
I am building a simple to-do list api using ASP.Net Core. It has two main two main models, a List model and a Task model. Each List has many Tasks. I build the models like this:
List Model:
namespace ToDoList.Models
{
public class List
{
[Key]
public int ListId { get; set; }
[Required]
[StringLength(25)]
public string Title { get; set; }
public string Colour { get; set; }
public virtual ICollection<Task> Tasks { get; set; }
public List()
{
Tasks = new List<Task>();
Colour = "secondary";
}
}
}
Task Model:
namespace ToDoList.Models
{
public class Task
{
[Key]
public int TaskId { get; set; }
[Required]
[StringLength(50)]
public string Title { get; set; }
public bool Done { get; set; }
public int ListId { get; set; }
public virtual List List { get; set; }
public Task()
{
Done = false;
}
}
}
When I send a post request to create a new task I am struggling to get the created task to be added to the Icollection part of the List model.
My Controller looks like this:
// POST: api/Tasks
[HttpPost]
public async Task<ActionResult<Models.Task>> PostTask(Models.Task task)
{
_context.Tasks.Add(task);
await _context.SaveChangesAsync();
return CreatedAtAction("GetTask", new { id = task.TaskId }, task);
}
If I send this data as JSON as a POST request:
{ title: "A New Task", listId: 11 }
I create this Task:
{"taskId":16,"title":"A New Task","done":false,"listId":11,"list":null}
As you can see it has the right listId but the list attached is null.
Also the task does not get added to the list.Tasks collection.
{"listId":11,"title":"Learn ASP.Net Core","colour":"secondary","tasks":[]}
As you can see tasks is still empty.
How do I get it set up that when ever a task is created it is always add to List.Tasks and then Tasks.List has the correct list attached to it, not null.
Also On my SQL Sever Database I expected to see a Tasks in the Lists table but I don't. Can anyone explain why?
SQL Sever Database Columns Picture
You could load the List entity from your DbContext and add it to the Task object you are returning:
[HttpPost]
public async Task<ActionResult<Models.Task>> PostTask(Models.Task task)
{
_context.Tasks.Add(task);
await _context.SaveChangesAsync();
task.List = _context.Lists.Single(task.ListId);
return CreatedAtAction("GetTask", new { id = task.TaskId }, task);
}
or you could return an instance of the Task loaded from the DbContext with included List:
var taskFromDb = _context.Tasks.Include(x => x.List).Single(x => x.Id = task.Id);
return CreatedAtAction("GetTask", new { id = task.TaskId }, taskFromDb);
To get a list with tasks, it needs to be loaded from the DbContext:
var listWithTasks = _context.Lists.Include(x => x.Tasks).Single(x => x.Id == task.ListId);
I am building a new project for browsing through movies and giving your opinion for them. Now I am on the administration part and I added functionality for adding a movie but when I try to add a movie the automapper throws exception for unmapped members on the service where I am mapping dto to data model. The members are from the base data model for example the id.
EDIT:
I tried to ignore all the members that make this exception, also tried to made a constructor with no arguments but doesn't work.
// Initialization
Mapper.Initialize(conf =>
{
conf.CreateMap<Movie, MovieDto>();
conf.CreateMap<MovieDto, Movie>();
conf.CreateMap<MovieDto, MovieViewModel>();
});
// Base Data Model
public class DataModel
{
[Key]
public int Id { get; set; }
[DataType(DataType.DateTime)]
public DateTime? CreatedOn { get; set; }
[DataType(DataType.DateTime)]
public DateTime? ModifiedOn { get; set; }
public bool IsDeleted { get; set; }
[DataType(DataType.DateTime)]
public DateTime? DeletedOn { get; set; }
}
// Movie Data Model
public class Movie: DataModel
{
public Movie(string title, double rating, string duration, string type, string description, DateTime releaseDate, string producer)
{
this.Title = title;
this.Rating = rating;
this.Duration = duration;
this.Type = type;
this.Description = description;
this.ReleaseDate = releaseDate;
this.Producer = producer;
}
// successfully mapped members
}
// Movie DTO
public class MovieDto
{
public string Title { get; set; }
public double Rating { get; set; }
public string Duration { get; set; }
public string Type { get; set; }
public string Description { get; set; }
public DateTime ReleaseDate { get; set; }
public string Producer { get; set; }
}
// Add functionality
public void AddMovie(MovieDto movie)
{
//execption here
var movieDM = this.mapper.Map<Movie>(movie);
this.repo.Add(movieDM);
this.saver.SaveChanges();
}
This is the exception on img: https://i.imgur.com/RGZP6NP.png
Got it to work by doing the following.
Firstly, since DataModel is a base class, I followed automapper's mapping inheritance (see docs).
Then since you are using a mapper instance to map this.mapper.Map<Movie>(movie), the configuration needs to be instance rather than static as well, and I use the AutoMapper.Extensions.Microsoft.DependencyInjection nuget package for this, which allows registering Automapper with the IoC container.
My configuration looks like this (inside the ConfigureServices method of the Startup class).
services.AddAutoMapper(conf =>
{
conf.CreateMap<object, DataModel>()
.ForMember(d => d.Id, opts => opts.Ignore())
.ForMember(d => d.CreatedOn, opts => opts.MapFrom(_ => DateTime.Now))
.ForMember(d => d.ModifiedOn, opts => opts.MapFrom(_ => DateTime.Now))
.ForMember(d => d.DeletedOn, opts => opts.MapFrom(_ => (DateTime?)null))
.ForMember(d => d.IsDeleted, opts => opts.MapFrom(_ => false))
.Include<MovieDto, Movie>();
conf.CreateMap<Movie, MovieDto>();
conf.CreateMap<MovieDto, Movie>();
});
Note that I used CreateMap<object, DataModel> for the base class mapping and just used hardcoded values for dates there, feel free to adjust to suit your scenario.
After injecting an instance of IMapper, I was able to call this.mapper.Map<Movie>(movie) successfully.
Hope this sets u off in a good direction.
You can specify that AutoMapper should not validate that all properties are being mapped. The MemberList enum can be used for this when creating the mapping configuration. For example:
conf.CreateMap<MovieDto, Movie>(MemberList.None)
The error in the screenshot however indicates that another mapping is problematic, the one from MovieViewModel to MovieDto. I suggest you add a mapping configuration for these types as well:
conf.CreateMap<MovieViewModel, MovieDto>(MemberList.None)
You could try Profile Instances.
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap<OrderViewModel, Order>()
.ForMember(dest => dest.OrderItem, opt => opt.MapFrom(src => src.OrderItemViewModel));
CreateMap<OrderItemViewModel, OrderItem>();
CreateMap<Order, Order>()
.ForMember(dest => dest.Id, opt => opt.Ignore());
CreateMap<Movie, MovieDto>();
CreateMap<MovieDto, Movie>();
}
}
Here is the working demo AutoMapperProfile
How to solve one to many relational issue in asp.net?
I have Topic which contain many playlists.
My code:
public class Topic
{
public int Id { get; set; }
public String Name { get; set; }
public String Image { get; set; }
---> public virtual List<Playlist> Playlist { get; set; }
}
and
public class Playlist
{
public int Id { get; set; }
public String Title { get; set; }
public int TopicId { get; set; }
---> public virtual Topic Topic { get; set; }
}
My controller function
[Route("data/binding/search")]
public JsonResult Search()
{
var search = Request["term"];
var result= from m in _context.Topics where m.Name.Contains(search) select m;
return Json(result, JsonRequestBehavior.AllowGet);
}
When I debug my code I will see an infinite data because Topics will call playlist then playlist will call Topics , again the last called Topic will recall playlist and etc ... !
In general when I just use this relation to print my data in view I got no error and ASP.NET MVC 5 handle the problem .
The problem happens when I tried to print the data as Json I got
Is there any way to prevent an infinite data loop in JSON? I only need the first time of data without call of reference again and again
You are getting the error because your entity classes has circular property references.
To resolve the issue, you should do a projection in your LINQ query to get only the data needed (Topic entity data).
Here is how you project it to an anonymous object with Id, Name and Image properties.
public JsonResult Search(string term)
{
var result = _context.Topics
.Where(a => a.Name.Contains(term))
.Select(x => new
{
Id = x.Id,
Name = x.Name,
Image = x.Image
});
return Json(result, JsonRequestBehavior.AllowGet);
}
If you have a view model to represent the Topic entity data, you can use that in the projection part instead of the anonymous object
public class TopicVm
{
public int Id { set;get;}
public string Name { set;get;}
public string Image { set;get;}
}
public JsonResult Search(string term)
{
var result = _context.Topics
.Where(a => a.Name.Contains(term))
.Select(x => new TopicVm
{
Id = x.Id,
Name = x.Name,
Image = x.Image
});
return Json(result, JsonRequestBehavior.AllowGet);
}
If you want to include the Playlist property data as well, you can do that in your projection part.
public JsonResult Search(string term)
{
var result = _context.Topics
.Where(a => a.Name.Contains(term))
.Select(x => new
{
Id = x.Id,
Name = x.Name,
Image = x.Image,
Playlist = x.Playlist
.Select(p=>new
{
Id = p.Id,
Title = p.Title
})
});
return Json(result, JsonRequestBehavior.AllowGet);
}