How to use Moq with Entity Framework Include() - c#

I am trying write unit test for my DAL layer.
The complication is that the DAL Layer has a query which uses Include().
I don't know how to mock the Include() method.
Models
public class Apps
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[DataMember]
public int ID { get; set; }
[DataMember]
[Required(ErrorMessage = "App name required.")]
public string Name { get; set; }
public virtual ICollection<AppDataPermission> AppDataPermissions { get; set; }
}
public class AppDataPermission{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[DataMember]
public int ID { get; set; }
public DataPermissions DataPermission { get; set; }
public virtual Apps App { get; set; }
}
public enum DataPermissions
{
Admin = 1,
Support = 2
}
DAL
public List<Apps> GetApps()
{
var apps = dbContext.Apps
.Include(x => x.AppDataPermission)
.ToList();
return apps;
}
I tried following [https://msdn.microsoft.com/en-us/library/dn314429(v=vs.113).aspx][1]
But I get following error
System.ArgumentNullException occurred
HResult=0x80004003
Message=Value cannot be null.
Parameter name: source
Source=EntityFramework
StackTrace:
at System.Data.Entity.Utilities.Check.NotNull[T](T value, String parameterName)
at System.Data.Entity.QueryableExtensions.Include[T,TProperty](IQueryable`1 source, Expression`1 path)

The problem with mocking the database access code is that the you are mocking the part that is the most complicated (linq-to-sql, navigation properties).
I recommend, generally speaking, not to expose DAL objects.
Anyway, you haven't shown your mocking code, but I'm assuming that you are mocking public List<Apps> GetApps().
Here is one method to mock it:
var mockRepo = new Mock<IMyAmazingRepository>(MockBehavior.Strict);
var myMockedApps = new List<App> () {
new Apps { ID = 1, Name ="One", new List<AppDataPermission> { (...) },
new Apps { ID = 2, Name ="Two", new List<AppDataPermission> { (...) }
};
mockRepo.Setup(m => m.GetApps(_loggedInUserId)).Returns(myMockedApps);

Related

AutoMapper using EF Core persists and inheritance

I am using
.NET Core v3.1
EF Core v3
AutoMapper v9
AutoMapper.Collection.EntityFrameworkCore v1.0.1
and I am trying to add polymorph entities.
I have the following entities:
public abstract class Base
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class Child1 : Base
{
public string Name2 { get; set; }
}
And corresponding dtos
public abstract class BaseDto
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class Child1Dto : BaseDto
{
public string Name2 { get; set; }
}
I am registering them like that:
CreateMap<Base, BaseDto>()
.IncludeAllDerived()
.ReverseMap()
.IncludeAllDerived();
CreateMap<Child1, Child1Dto>()
.ReverseMap();
And also add do db context like that with discriminator:
modelBuilder.Entity<Base>()
.HasDiscriminator<string>("type")
.HasValue<Child1>("child1");
After that I try to add a new entry to the database like that:
var dto = new Child1Dto()
{
Name = "Name",
Name2 = "Name2",
};
var addedOrInserted = db.Set<Base>().Persist(_mapper).InsertOrUpdate(dto);
But for that case, I get the following exception:
Expression of type 'AutoMapper.EquivalencyExpression.EquivalentExpression2[Child1]' cannot be used for parameter of type 'AutoMapper.EquivalencyExpression.IEquivalentComparer2[Base]'
I think that this happens because the InsertOrUpdate is checking if the entity exists to check if an insert or an update should be performed.
Do you have any solution for that?
It seems to be because you used CreateMap<Child1,Child1Dto>().ReverseMap(); when registering.
Even if they are inheritance relationship, it is not possible to directly add Child1Dto to Base, I think it may be necessary to use db.Set<Child1>.
Try to add public DbSet<Child1> Child1s { get; set; }in your context.
Then in your controller:
Child1Dto basedto = new Child1Dto()
{
Id = new Guid(),
Name = "Name",
Name2 = "Name2"
};
context.Child1s.Persist(mapper).InsertOrUpdate(basedto);
context.SaveChanges();
Result:

How to work only with the Entity Framework Scaffolded Models from Database

I have been working with .Net Core Entity Framework database first approach with the Scaffolding technique.
It generated me a couple Models/Classes from my Database Tables, but for now, I will just minimize the issue I am having to this two tables... a relation one to many on the both ChampionID column:
So, after scaffolding/mapping the models with EntityCore Tools it generated the following two classes (and several others that are not relevant):
Champion.cs:
public partial class Champion
{
public Champion()
{
ChampionScreenshot = new HashSet<ChampionScreenshot>();
ChampionUser = new HashSet<ChampionUser>();
ChampionUserRate = new HashSet<ChampionUserRate>();
}
public int ChampionId { get; set; }
public string Name { get; set; }
public string Nickname { get; set; }
public string Description { get; set; }
public string ImagePath { get; set; }
public byte AttackDamageScore { get; set; }
public byte AbilityPowerScore { get; set; }
public byte ResistanceScore { get; set; }
public byte PlayingDifficult { get; set; }
public int PrimaryClassId { get; set; }
public int SecondaryClassId { get; set; }
public ChampionClass PrimaryClass { get; set; }
public ChampionClass SecondaryClass { get; set; }
public ICollection<ChampionScreenshot> ChampionScreenshot { get; set; }
public ICollection<ChampionUser> ChampionUser { get; set; }
public ICollection<ChampionUserRate> ChampionUserRate { get; set; }
}
ChampionScreenshot.cs:
public partial class ChampionScreenshot
{
public int ChampionScreenshotId { get; set; }
public string ImagePath { get; set; }
public int ChampionId { get; set; }
public Champion Champion { get; set; }
}
My doubt is: what is the correct way to retrieve a Champion object with the ChampionScreenshot attribute filled?
For example, this is what I am doing in my Service layer:
public async Task<Champion> GetChampion(int id)
{
Champion champion = await _context.Champion.FirstAsync(m => m.ChampionId == id);
champion.ChampionScreenshot = _context.ChampionScreenshot.ToListAsync().Result.FindAll(m => m.ChampionId == champion.ChampionId);
return champion;
}
So I am basically getting a specific Champion and then filling the ChampionScreenshot attribute (which is also a Class) separately, but the thing is that inside my ChampionScreenshot there is also a Champion class attribute which fully loads once again:
Which is obviously generating an error once it is exposed in the endpoint of the Restful Service:
[Produces("application/json")]
[Route("api/Champions")]
public class ChampionsController : Controller
{
[HttpGet("{id}")]
public async Task<IActionResult> GetChampion([FromRoute] int id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var champion = await _service.GetChampion(id);
if (champion == null)
{
return NotFound();
}
return Ok(champion);
}
...
Error:
Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'champion' with type 'ChampionsService.Models.Champion'. Path 'championScreenshot[0]'.
So, I was thinking in just creating my custom model and fill it with the data extracted from my DbContext instead of returning the models already created but I really think that there should be a way to fully use only the mapped Models, I was wondering that...
Champion references itself:
Champion > multiple ChampionScreenshot > Champion (back to the original object)
That's easy to solve:
return Json(champion, new JsonSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore });
Or you could do it for the entire application:
services.AddMvc().AddJsonOptions(opts =>
{
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
And then just:
return Json(champion);
The following troubles me, though:
Champion champion = await _context.Champion.FirstAsync(m => m.ChampionId == id);
champion.ChampionScreenshot = _context.ChampionScreenshot.ToListAsync().Result.FindAll(m => m.ChampionId == champion.ChampionId);
You are saying "go to the database, download every single championscreenshot and find the ones I want through an in-memory search". That's not only horrible slow, it also wastes a lot of resources in your application and in the database. For including data, you use Include:
Champion champion = await _context.Champion
.Include(x => x.ChampionScreenshot)
.FirstAsync(x => x.ChampionId == id);
(this says "go to the database and bring me the champion but also include all the ChampionScreenshot that correspond, through an inner join).

Storing list of custom objects in Entity Framework

Started to learn asp.net and DB manipulations. Trying to implement some simple functionality - two models, one has list of references to another.
Here is an error that I currently get:
An exception occurred while initializing the database. See the InnerException for details.
Inner exception:
Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values.
My models:
public class Killer
{
public Killer(string name, string biography)
{
Name = name;
Biography = biography;
KillerId = Guid.NewGuid();
}
public Guid KillerId { get; set; }
public string Name { get; set; }
public string Biography { get; set; }
public virtual Contract Contract { get; set; }
}
public class Contract
{
public Contract(Status status, Killer target, string description, params Killer[] targets)
{
ContractId = Guid.NewGuid();
this.status = status;
Target = target;
Description = description;
Killers = new HashSet<Killer>();
foreach (var t in targets) Killers.Add(t);
}
public Guid ContractId { get; set; }
public enum Status { active, done, failed, rejected, abandoned }
public Status status { get; set; }
public Killer Target { get; set; }
public string Description { get; set; }
//[ForeignKey("ContractID")]
public virtual ICollection<Killer> Killers { get; set; }
}
In context I initialize db with lists of objects
public class KillerContext : DbContext
{
public DbSet<Killer> Killers { get; set; }
public DbSet<Contract> Contracts { get; set; }
}
In controller I do:
KillerContext k = new KillerContext();
public ActionResult Index()
{
var contracts = k.Contracts.ToList();
ViewBag.contracts = contracts;
return View();
}
In Global.asax:
Database.SetInitializer(new KillerContextInitialization());
Here is how I enter first data in db:
public sealed class KillerContextInitialization : DropCreateDatabaseAlways<KillerContext>
{
protected override void Seed(KillerContext db)
{
List<Killer> killers = new List<Killer>();
//List<Contract> contracts = new List<Contract>();
killers.Add(new Killer(name: "Ivan Firstein", biography: "He was born in the shadows."));
killers.Add(new Killer(name: "Oleg Gazmanov", biography: "test man"));
db.Contracts.Add(new Contract(
Contract.Status.active,
killers.SingleOrDefault(x => x.Name == "Ivan Firstein"),
"KILL OR BE KILLED. As always with love.",
killers.SingleOrDefault(x => x.Name == "Oleg Gazmanov")
));
db.Killers.AddRange(killers);
base.Seed(db);
}
}
Looks like you need add ForeignKey attribute for killer Model, and store this key in property ContractId:
public class Killer
{
[ForeignKey(nameof(ContractId)] //Name of added property in line below
public Contract Contract { get; set; } //no need "virtual"
public Guid? ContractId { get; set; }
// other properties...
}
public class Contract
{
[ForeignKey("ContractId")] //Name of added property in Killer Model
public virtual ICollection<Killer> Killers { get; set; }
// other code...
}
EDIT
You should do something similar to the Contract.Target property:
[ForeignKey(nameof(TargetId)]
public Killer Target { get; set; }
public Guid TargetId { get; set; }
For enum types you should add attributes like this:
[Column(nameof(status), TypeName = "int")]
public Status status { get; set; }
Find out that problem was in public Killer Target { get; set; }
When i was adding data, that field was considered as NOT NULL, and all what i need to do, is save changes after filling killers, like so:
public sealed class KillerContextInitialization : DropCreateDatabaseAlways<KillerContext>
{
protected override void Seed(KillerContext db)
{
List<Killer> killers = new List<Killer>();
killers.Add(new Killer(name: "Ivan Firstein", biography: "He was born in the shadows."));
killers.Add(new Killer(name: "Oleg Gazmanov", biography: "test man"));
db.SaveChanges(); // - save killers first, then add them to contract
db.Contracts.Add(new Contract(
Contract.Status.active,
killers.SingleOrDefault(x => x.Name == "Ivan Firstein"),
"KILL OR BE KILLED. As always with love.",
killers.SingleOrDefault(x => x.Name == "Oleg Gazmanov")
));
db.Killers.AddRange(killers);
base.Seed(db);
}
}

How to store JSON in an entity field with EF Core?

I am creating a reusable library using .NET Core (targeting .NETStandard 1.4) and I am using Entity Framework Core (and new to both). I have an entity class that looks like:
public class Campaign
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
public JObject ExtendedData { get; set; }
}
and I have a DbContext class that defines the DbSet:
public DbSet<Campaign> Campaigns { get; set; }
(I am also using the Repository pattern with DI, but I don't think that is relevant.)
My unit tests give me this error:
System.InvalidOperationException: Unable to determine the relationship
represented by navigation property 'JToken.Parent' of type
'JContainer'. Either manually configure the relationship, or ignore
this property from the model..
Is there a way to indicate that this is not a relationship but should be stored as a big string?
Going to answer this one differently.
Ideally the domain model should have no idea how data is stored. Adding backing fields and extra [NotMapped] properties is actually coupling your domain model to your infrastructure.
Remember - your domain is king, and not the database. The database is just being used to store parts of your domain.
Instead you can use EF Core's HasConversion() method on the EntityTypeBuilder object to convert between your type and JSON.
Given these 2 domain models:
public class Person
{
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string FirstName { get; set; }
[Required]
[MaxLength(50)]
public string LastName { get; set; }
[Required]
public DateTime DateOfBirth { get; set; }
public IList<Address> Addresses { get; set; }
}
public class Address
{
public string Type { get; set; }
public string Company { get; set; }
public string Number { get; set; }
public string Street { get; set; }
public string City { get; set; }
}
I have only added attributes that the domain is interested in - and not details that the DB would be interested in; I.E there is no [Key].
My DbContext has the following IEntityTypeConfiguration for the Person:
public class PersonsConfiguration : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
// This Converter will perform the conversion to and from Json to the desired type
builder.Property(e => e.Addresses).HasConversion(
v => JsonConvert.SerializeObject(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
v => JsonConvert.DeserializeObject<IList<Address>>(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }));
}
}
With this method you can completely decouple your domain from your infrastructure. No need for all the backing field & extra properties.
The key to making the the Change Tracker function correctly is to implement a ValueComparer as well as a ValueConverter. Below is an extension to implement such:
public static class ValueConversionExtensions
{
public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder) where T : class, new()
{
ValueConverter<T, string> converter = new ValueConverter<T, string>
(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<T>(v) ?? new T()
);
ValueComparer<T> comparer = new ValueComparer<T>
(
(l, r) => JsonConvert.SerializeObject(l) == JsonConvert.SerializeObject(r),
v => v == null ? 0 : JsonConvert.SerializeObject(v).GetHashCode(),
v => JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(v))
);
propertyBuilder.HasConversion(converter);
propertyBuilder.Metadata.SetValueConverter(converter);
propertyBuilder.Metadata.SetValueComparer(comparer);
propertyBuilder.HasColumnType("jsonb");
return propertyBuilder;
}
}
Example of how this works.
public class Person
{
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string FirstName { get; set; }
[Required]
[MaxLength(50)]
public string LastName { get; set; }
[Required]
public DateTime DateOfBirth { get; set; }
public List<Address> Addresses { get; set; }
}
public class Address
{
public string Type { get; set; }
public string Company { get; set; }
public string Number { get; set; }
public string Street { get; set; }
public string City { get; set; }
}
public class PersonsConfiguration : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
// This Converter will perform the conversion to and from Json to the desired type
builder.Property(e => e.Addresses).HasJsonConversion<IList<Address>>();
}
}
This will make the ChangeTracker function correctly.
#Michael's answer got me on track but I implemented it a little differently. I ended up storing the value as a string in a private property and using it as a "Backing Field". The ExtendedData property then converted JObject to a string on set and vice versa on get:
public class Campaign
{
// https://learn.microsoft.com/en-us/ef/core/modeling/backing-field
private string _extendedData;
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
[NotMapped]
public JObject ExtendedData
{
get
{
return JsonConvert.DeserializeObject<JObject>(string.IsNullOrEmpty(_extendedData) ? "{}" : _extendedData);
}
set
{
_extendedData = value.ToString();
}
}
}
To set _extendedData as a backing field, I added this to my context:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Campaign>()
.Property<string>("ExtendedDataStr")
.HasField("_extendedData");
}
Update: Darren's answer to use EF Core Value Conversions (new to EF Core 2.1 - which didn't exist at the time of this answer) seems to be the best way to go at this point.
For those using EF 2.1 there is a nice little NuGet package EfCoreJsonValueConverter that makes it pretty simple.
using Innofactor.EfCoreJsonValueConverter;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class Campaign
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
public JObject ExtendedData { get; set; }
}
public class CampaignConfiguration : IEntityTypeConfiguration<Campaign>
{
public void Configure(EntityTypeBuilder<Campaign> builder)
{
builder
.Property(application => application.ExtendedData)
.HasJsonValueConversion();
}
}
I have made a solution based on Robert Raboud's contribution. The change made by me is that my implementation uses a HasJsonConversion method that depends on the System.Text.Json package rather than Newtonsofts library:
public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder) where T : class, new()
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true
};
ValueConverter<T, string> converter = new ValueConverter<T, string>
(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<T>(v, options) ?? new T()
);
ValueComparer<T> comparer = new ValueComparer<T>
(
(l, r) => JsonSerializer.Serialize(l, options) == JsonSerializer.Serialize(r, options),
v => v == null ? 0 : JsonSerializer.Serialize(v, options).GetHashCode(),
v => JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(v, options), options)
);
propertyBuilder.HasConversion(converter);
propertyBuilder.Metadata.SetValueConverter(converter);
propertyBuilder.Metadata.SetValueComparer(comparer);
propertyBuilder.HasColumnType("LONGTEXT");
return propertyBuilder;
}
Note also that this implementation expects for the column to be LONGTEXT since I am using a MySQL setup.
Here's something I used
Model
public class FacilityModel
{
public string Name { get; set; }
public JObject Values { get; set; }
}
Entity
[Table("facility", Schema = "public")]
public class Facility
{
public string Name { get; set; }
public Dictionary<string, string> Values { get; set; } = new Dictionary<string, string>();
}
Mapping
this.CreateMap<Facility, FacilityModel>().ReverseMap();
DBContext
base.OnModelCreating(builder);
builder.Entity<Facility>()
.Property(b => b.Values)
.HasColumnType("jsonb")
.HasConversion(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<Dictionary<string, string>>(v));
Could you try something like this?
[NotMapped]
private JObject extraData;
[NotMapped]
public JObject ExtraData
{
get { return extraData; }
set { extraData = value; }
}
[Column("ExtraData")]
public string ExtraDataStr
{
get
{
return this.extraData.ToString();
}
set
{
this.extraData = JsonConvert.DeserializeObject<JObject>(value);
}
}
here is the migration output:
ExtraData = table.Column<string>(nullable: true),
For developers, who work with EF Core 3.1 and meet such error ("The entity type 'XXX' requires a primary key to be defined. If you intended to use a keyless entity type call 'HasNoKey()'.") the solution is:
Move .HasConversion() method with it's lambda from:
OrderConfiguration : IEntityTypeConfiguration<T> to
OnModelCreating(ModelBuilder modelBuilder) in your DataContext.
// DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var entityTypes = modelBuilder.Model.GetEntityTypes();
foreach (var entityType in entityTypes)
{
foreach (var property in entityType.ClrType.GetProperties().Where(x => x != null && x.GetCustomAttribute<HasJsonConversionAttribute>() != null))
{
modelBuilder.Entity(entityType.ClrType)
.Property(property.PropertyType, property.Name)
.HasJsonConversion();
}
}
base.OnModelCreating(modelBuilder);
}
Create an attribute to handle the properties of the entities.
public class HasJsonConversionAttribute : System.Attribute
{
}
Create extention class to find Josn properties
public static class ValueConversionExtensions
{
public static PropertyBuilder HasJsonConversion(this PropertyBuilder propertyBuilder)
{
ParameterExpression parameter1 = Expression.Parameter(propertyBuilder.Metadata.ClrType, "v");
MethodInfo methodInfo1 = typeof(Newtonsoft.Json.JsonConvert).GetMethod("SerializeObject", types: new Type[] { typeof(object) });
MethodCallExpression expression1 = Expression.Call(methodInfo1 ?? throw new Exception("Method not found"), parameter1);
ParameterExpression parameter2 = Expression.Parameter(typeof(string), "v");
MethodInfo methodInfo2 = typeof(Newtonsoft.Json.JsonConvert).GetMethod("DeserializeObject", 1, BindingFlags.Static | BindingFlags.Public, Type.DefaultBinder, CallingConventions.Any, types: new Type[] { typeof(string) }, null)?.MakeGenericMethod(propertyBuilder.Metadata.ClrType) ?? throw new Exception("Method not found");
MethodCallExpression expression2 = Expression.Call(methodInfo2, parameter2);
var converter = Activator.CreateInstance(typeof(ValueConverter<,>).MakeGenericType(typeof(List<AttributeValue>), typeof(string)), new object[]
{
Expression.Lambda( expression1,parameter1),
Expression.Lambda( expression2,parameter2),
(ConverterMappingHints) null
});
propertyBuilder.HasConversion(converter as ValueConverter);
return propertyBuilder;
}
}
Entity example
public class Attribute
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
[HasJsonConversion]
public List<AttributeValue> Values { get; set; }
}
public class AttributeValue
{
public string Value { get; set; }
public IList<AttributeValueTranslation> Translations { get; set; }
}
public class AttributeValueTranslation
{
public string Translation { get; set; }
public string CultureName { get; set; }
}
Download Source
For those who are working on entity framework core 5.0 and above. below can work if you are getting error like below
The entity type '{EntityName}' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'. The error is asking to define a primary key on the model
Try this
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Person>(
eb =>
{
eb.Property(p => p.Addresses).HasConversion(
v => JsonConvert.SerializeObject(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
v => JsonConvert.DeserializeObject<IList<Address>>(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })
);
});
}
----------------- 2022 Update -----------------
Hey there,
Just sharing an update from Dec/2022.
Recently, EF Core 7.0 was launched, containing one of the most awaited features called JSON Columns.
This new feature allows us for mapping aggregates (written from .NET types) into JSON documents.
Just remember that, In EF Core, aggregate types are defined using Owned Entity Types.
https://learn.microsoft.com/en-us/ef/core/modeling/owned-entities
Let's consider this scenario: A object called "LogDetail", that needs to be stored into a single column as a JSON, inside another object (or table) called "Log".
public class Log : BaseEntity
{
public string TraceID { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
public LogDetail LogDetail { get; set; } = null!;
public string IpAddress { get; set; } = string.Empty;
}
public class LogDetail
{
public string InnerException { get; set; } = null!;
public string MemberMap { get; set; } = null!;
public string Message { get; set; } = null!;
public string Source { get; set; } = null!;
public string StackTrace { get; set; } = null!;
public string TypeMap { get; set; } = null!;
public string Path { get; set; } = null!;
}
By the end, all that you need is to configure the behavior on your configuration map class:
public sealed class LogMap : IEntityTypeConfiguration<Log>
{
public override void Configure(EntityTypeBuilder<Log> builder)
{
/* Owned Type Configurations */
builder.OwnsOne(e => e.LogDetail, options =>
{
options.ToJson("LOG_DETAIL");
});
}
}
The comment by #Métoule:
Be careful with this approach: EF Core marks an entity as modified only if the field is assigned to. So if you use person.Addresses.Add, the entity won't be flagged as updated; you'll need to call the property setter person.Addresses = updatedAddresses.
made me take a different approach so that this fact is obvious: use Getter and Setter methods, rather than a property.
public void SetExtendedData(JObject extendedData) {
ExtendedData = JsonConvert.SerializeObject(extendedData);
_deserializedExtendedData = extendedData;
}
//just to prevent deserializing more than once unnecessarily
private JObject _deserializedExtendedData;
public JObject GetExtendedData() {
if (_extendedData != null) return _deserializedExtendedData;
_deserializedExtendedData = string.IsNullOrEmpty(ExtendedData) ? null : JsonConvert.DeserializeObject<JObject>(ExtendedData);
return _deserializedExtendedData;
}
You could theoretically do this:
campaign.GetExtendedData().Add(something);
But it's much more clear that That Doesn't Do What You Think It Does™.
If you're using database-first and using some kind of class auto-generator for EF, then the classes will usually be declared as partial, so you can add this stuff in a separate file that won't get blown away the next time you update your classes from your database.

Autommaper error in second project

i have problem with my project WebApi, by using AutoMapper.
In my solution i use AutoMapper v4.1.1, solution old and have a lot of code inside.
Where is 4 projects
Core - service and extention
DataLayer - DataLayer and repository
MVC - standart web form project
WebApi - new project with Api functionality
in this solution i use:
Entity FrameWork 6.1.1
AutoMapper 4.1.1
connection strings in WebConfig to local DB base on SqlServer.
i have two models, User - its DataBaseEntity, and UserModel - this is BussinesModel
My AutoMapper class in DataLayer looks so
namespace DataLayer
{
internal static class Mapping
{
static Mapping()
{
RegisterMappings();
}
public static TDestination Map<TSource, TDestination>(TSource source, TDestination destination)
{
return Mapper.Map(source, destination);
}
private static void RegisterMappings()
{
try
{
Mapper.AllowNullDestinationValues = true;
//Mapping instruction for User
Mapper.CreateMap<User, UserModel>().IgnoreAllPropertiesWithAnInaccessibleSetter().ReverseMap();
Mapper.AssertConfigurationIsValid();
}
}
}
in CoreLayer i use this code
public AuthenticationResult Login(string login, string password)
{
if (string.IsNullOrEmpty(login))
throw new ArgumentNullException(nameof(login));
if (string.IsNullOrEmpty(password))
throw new ArgumentNullException(nameof(password));
using (var db = new Context())
{
try
{
//Here my UserDbEntity model, and it already connected from Context
var dbUser = db.Users.AsNoTracking().FirstOrDefault(u => u.Login == login && u.IsActive && !u.IsDeleted);
//mapping model
var model = Mapping.Map(dbUser, new UserModel());
//Some result logic here
return result;
}
}
}
all references to Projects inside WebApi and WebFroms Mvc project is same (references to DataLayer and CoreLayer)
and now the problem, than i call action
CoreLayer.Login(string login, string pass)
from my MVC project, he return me correctly data
and if i do so from Web Api project he show me error
Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
==================================================================
ServiceModel ->Service (Source member list)
Core.Models.ServiceModel -> Data.DB.Service (Source member list)
Unmapped properties:
Specialization
ServiceTechnic
ServiceTools
RefRescueWorkTypes
all this unmapped members is complex objects (model inside model), and have own rules to mapping. While i working in MVC project, they mappet correctly
P.S Sorry for my English, it`s not my native language
UPD 1 for #Alexandru Marculescu
example of unmmaped models,
Source
PersonalInformation Entity
internal partial class PersonalInformation
{
public override System.Guid Id { get; set; }
public string FullName { get; set; }
//link to related Address entity
public Nullable<System.Guid> AddressId { get; set; }
public string EMail { get; set; }
// a lot of string properties here, nothing intresting
internal virtual Address Address { get; set; }
}
Address entity
internal partial class Address
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Address()
{
this.PersonalInformations = new HashSet<PersonalInformation>();
}
public override System.Guid Id { get; set; }
//hardcoded Id inside code
public System.Guid RegionId { get; set; }
public System.Guid DistrictId { get; set; }
public Nullable<System.Guid> LocalityId { get; set; }
public string Street { get; set; }
public string House { get; set; }
// a lot of string properties here, nothing intresting
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
internal virtual ICollection<PersonalInformation> PersonalInformations { get; set; }
}
bussines model
public class AddressModel
{
public Guid Id {get;set;}
public Guid RegionId { get; set; }
public Guid DistrictId { get; set; }
public Guid? LocalityId { get; set; }
public string Street { get; set; }
public string House { get; set; }
//one-to-many
public List<PersonalInformationModel> PersonalInformationModels {get;set;}
}
public class PersonalInformationModel
{
public Guid Id {get;set;}
public string FullName{ get; set; }
public Guid? AddressId { get; set; }
public string EMail { get; set; }
private AddressModel _address;
public AddressModel Address
{
get
{
if (AddressId.HasValue && App.CreateCommonService().Addresses.Get(AddressId.Value).Value != null)
return App.CreateCommonService().Addresses.Get(AddressId.Value).Value;
return _address;
}
set { _address = value; }
}
}
and mapping instruction for this
internal static class Mapping
{
static Mapping()
{
RegisterMappings();
}
public static TDestination Map<TSource, TDestination>(TSource source, TDestination destination)
{
return Mapper.Map(source, destination);
}
private static void RegisterMappings()
{
try
{
Mapper.AllowNullDestinationValues = true;
Mapper.CreateMap<Address, AddressModel>().ForMember(x=>x.PersonalInformationModels , o=> o.MapFrom(s=>s.PersonalInformations.ToList())).IgnoreAllPropertiesWithAnInaccessibleSetter().ReverseMap();
Mapper.CreateMap<PersonalInformation, PersonalInformationModel>()
.ForMember(x => x.Address, o => o.Ignore()).IgnoreAllPropertiesWithAnInaccessibleSetter().ReverseMap();
Mapper.AssertConfigurationIsValid();
}
catch (Exception ex) when (App.CreateLogService().WriteError(ex))
{
if (Debugger.IsAttached)
Debug.WriteLine(ex);
}
}
}
And this how i call mapping from my services
var item = new PersonalInformationModel();
//some input logic and *item* become full
var dbEntity = Mapping.Map(item, new PersonalInformation());
var item = new AddressModel();
//some input logic and *item* become full
var dbEntity = Mapping.Map(item, new Address());
And for this classes i get error look like this
Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
==================================================================
AddressModel ->Service (Source member list)
Core.Models.AddressModel -> Data.DB.Address(Source member list)
Unmapped properties:
PersonalInformations
All references for Linq, Core, Entity frameworks same in both projects, .Net framework version 4.6, connection strings same in web.configs (in both projects) but MVC work good, an Api give me unmapped error.

Categories

Resources