Use Dapper.Contrib with inheritance - c#

When trying to use Contrib's CRUD methods in an object where the properties are in an inherited object I get an
Entity must have at least one [Key] or [ExplicitKey] property
error. Here is a simplified version of my objects:
public class BaseObject
{
public string Delete()
{
using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["con"].ConnectionString))
{
db.Delete(this);
}
}
}
and this
public class Product: BaseObject
{
[Key]
public int id { get; set; }
public string title { get; set; }
public string name { get; set; }
}
I get the error when I execute:
Product product = new Product() {id = 1};
product.Delete();
If I Remove the inheritance and move the Delete() method into the Product object it works flawlessly.
Any ideas?

Your BaseObject is not linked to any table so calling Delete() on it could not be understood by Dapper.
I think that in your case, I would simply use an extension method:
public static class BaseObjectExtensions
{
public static string Delete<T>(this T theObject) where T : BaseObject
{
using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["con"].ConnectionString))
{
db.Delete(theObject);
}
}
}

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:

Incorrect Syntax near keyword "From"

I have this custom repo and when I try execute a query, it returns an exception
If I execute the query using sql string, it does not return an error, but when I use some extention then I have this exception.
Like this:
public override IEnumerable<TableContrato> All()
{
//var query = "select * from Contrato";
//var data = Conn.Query<TableContrato>(query);
var data = Conn.GetList<TableContrato>();
return data;
}
All my entities are created in c# using "Table" Prefix, like TableContrato, and my table is called Contrato
This way, I've build a custom mapper, like this.
public class CustomMapper<TTableEntity> : PluralizedAutoClassMapper<TTableEntity> where TTableEntity : class
{
public override void Table(string tableName)
{
tableName = tableName.Replace("Table", string.Empty).Trim();
base.Table(tableName);
}
}
and this is my repo base
public abstract class ReadOnlyRepositoryBase<TEntity, TTable, TKey> : IReadOnlyRepository<TEntity, TKey>
where TEntity : class where TTable : class
{
protected IDbConnection Conn { get; set; }
protected DapperContext Context { get; private set; }
protected ReadOnlyRepositoryBase()
{
Context = new DapperContext();
Conn = Context.Connection;
InicializaMappings();
}
public void InicializaMappings()
{
global::DapperExtensions.DapperExtensions.DefaultMapper = typeof(CustomMapper<>);
}
}
and here is my exception.
I know I could do all with literal queries, but this way I cannot use Expression trees for filtering, neither generics.
What I'm doing wrong?
EDIT: 26/05/2015 - TableContrato
public class TableContrato
{
public Guid ContratoId { get; set; }
public Guid EmpresaId { get; set; }
public string ContratoNome { get; set; }
public string ContratoCodigo { get; set; }
public DateTime? DataDeCriacao { get; set; }
public Guid? UsuarioQueCriou { get; set; }
public TableEmpresaGrupo Empresa { get; set; }
public virtual ICollection<TableLocal> Locais { get; set; }
}
UPDATE - 31/05/2016 - Sql profiler
Here is an image of Sql Server Profile, of executed Sql.
Aparently, the '*' character is missed.
I think is a configuration error, so here is the map class
public class TableContratoMap : ClassMapper<TableContrato>
{
public TableContratoMap()
{
// ReSharper disable once RedundantBaseQualifier
base.Table("Contrato");
}
}
One more doubt... I'm familiar with EF mapping, where I don't need to map every single column.
Is it really neccessary in Dapper?
You need to call AutoMap within your class mapper. Once you call that, AutoMap will build the collection of fields internally and apply that to the field list within the SQL statement.
public class TableContratoMap : ClassMapper<TableContrato>
{
public TableContratoMap()
{
// ReSharper disable once RedundantBaseQualifier
base.Table("Contrato");
AutoMap();
}
}
One more doubt... I'm familiar with EF mapping, where I don't need to map every single column. Is it really neccessary in Dapper?
No just use dapper contrib and same names in db as code.
Table Persons
Id
Name
Birth
Class Persons
Id
Name
Birth
var person = dapper.Get<Persons>(22);

Castle ActiveRecord BelongsTo with NotNull=true not-null Exception

I'm having the a problem when trying to persist a many to one relationship using Castle ActiveRecord and I hope someone has a better idea than me with this, the idea is to save a single object with a list of dependant objects in a single Save().
I have these classes:
[ActiveRecord("SomeClass")
public class SomeClass : ActiveRecordValidationBase<SomeClass>
{
[PrimaryKey]
public virtual long Id { get; set; }
[HasMany(Cascade = ManyRelationCascadeEnum.AllDeleteOrphan, Inverse = false)]
public virtual IList<AnotherClass> SomeObjects { get; set; }
}
[ActiveRecord("AnotherClass")
public class AnotherClass : ActiveRecordValidationBase<AnotherClass>
{
[PrimaryKey]
public virtual long Id { get; set; }
[Property(NotNull = true, Unique = true, Length = 70)]
public string Something { get; set; }
[BelongsTo("SomeId", NotNull = true)]
public virtual SomeClass Parent { get; set; }
}
What I want to do is something like this
var someClass = new SomeClass
{
SomeObjects = new List<AnotherClass>
{
new AnotherClass
{
Something = "Hello"
}
}
};
someClass.Save();
But I get this error:
Hibernate.PropertyValueException: not-null property references a null or transient value
Is there a way I could do that without setting a reference to the parent to every object before calling save?
Thanks!
you have to override BeforeSave and/or Save in your class SomeClass
public virtual void Save()
{
using(Transaction t = new Transaction())
{
foreach(AnotherClass a in this.SomeObjects??new AnotherClass[]{})
{
a.Parent = this;
a.Save();
}
base.Save();
}
}
Greetings
Juy Juka

EF adding duplicate records into lookup/reference table

I have 3 tables,
1. AttributeTypes (Columns: AttributeId (PK), AttributeName, ..)
2. Location (Columns: locationId (PK), LocationName, ...)
3. LocationAttributeType (Columns: locationId (FK), AttributeId (FK))
Whenever I am trying to insert new location record along with its attribute type from GUI, it should create new record for Table- Location and LocationAttributeType. But EF trying to add new record in Table- AttributeTypes as well, which is just used as reference table and should not add new/duplicate records in it. How can I prevent that?
here is my code,
The model which GUI sends is,
public class LocationDataModel
{
[DataMember]
public int Id { get; set; }
[DataMember]
public string Code { get; set; }
[DataMember]
public List<AttributeTypeDataModel> AssignedAttributes = new List<AttributeTypeDataModel>();
}
public class AttributeTypeDataModel
{
protected AttributeTypeDataModel() {}
public AttributeTypeDataModel(int id) { this.Id = id; }
public AttributeTypeDataModel(int id, string name)
: this(id)
{
this.Name = name;
}
[DataMember]
public int Id { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public virtual ICollection<LocationDataModel> Locations { get; set; }
}
The Entities created by EF are,
public partial class Location
{
public Location()
{
this.AttributeTypes = new List<AttributeType>();
}
public Location(int campusId, string code)
: this()
{
CampusId = campusId; Code = code;
}
public int Id { get; set; }
public int CampusId { get; set; }
public string Code { get; set; }
public virtual ICollection<AttributeType> AttributeTypes { get; set; }
}
public partial class AttributeType
{
public AttributeType()
{
this.Locations = new List<Location>();
}
public int AttributeTypeId { get; set; }
public string AttributeTypeName { get; set; }
public virtual ICollection<Location> Locations { get; set; }
}
I have below code to Add these new location to database,
private IEnumerable<TEntity> AddEntities<TModel, TEntity, TIdentityType>
(IEnumerable<TModel> models, Func<TModel, TIdentityType> primaryKey,
IGenericRepository<TEntity, TIdentityType> repository)
{
var results = new List<TEntity>();
foreach (var model in models)
{
var merged = _mapper.Map<TModel, TEntity>(model);
var entity = repository.Upsert(merged);
results.Add(entity);
}
repository.Save();
return results.AsEnumerable();
}
I am using following generic repository to do entity related operations
public TEntity Upsert(TEntity entity)
{
if (Equals(PrimaryKey.Invoke(entity), default(TId)))
{
// New entity
return Context.Set<TEntity>().Add(entity);
}
else
{
// Existing entity
Context.Entry(entity).State = EntityState.Modified;
return entity;
}
}
public void Save()
{
Context.SaveChanges();
}
Whats wrong I am doing here?
The DbSet<T>.Add() method attaches an entire object graph as added. You need to indicate to EF that the 'reference' entity is actually already present. There are two easy ways to do this:
Don't set the navigation property to an object. Instead, just set the corresponding foreign key property to the right value.
You need to ensure that you don't load multiple instances of the same entity into your object context. After creating the context, load the full list of AttributeType entities into the context and create a Dictionary<> to store them. When you want to add an attribute to a Location retrieve the appropriate attribute from the dictionary. Before calling SaveChanges() iterate through the dictionary and mark each AttributeType as unchanged. Something like this:
using (MyContext c = new MyContext())
{
c.AttributeTypes.Add(new AttributeType { AttributeTypeName = "Fish", AttributeTypeId = 1 });
c.AttributeTypes.Add(new AttributeType { AttributeTypeName = "Face", AttributeTypeId = 2 });
c.SaveChanges();
}
using (MyContext c = new MyContext())
{
Dictionary<int, AttributeType> dictionary = new Dictionary<int, AttributeType>();
foreach (var t in c.AttributeTypes)
{
dictionary[t.AttributeTypeId] = t;
}
Location l1 = new Location(1, "Location1") { AttributeTypes = { dictionary[1], dictionary[2] } };
Location l2 = new Location(2, "Location2") { AttributeTypes = { dictionary[1] } };
// Because the LocationType is already attached to the context, it doesn't get re-added.
c.Locations.Add(l1);
c.Locations.Add(l2);
c.SaveChanges();
}
In this specific case you are using a many-to-many relationship, with EF automatically handling the intermediate table. This means that you don't actually have the FK properties exposed in the model, and my first suggestion above won't work.
Therefore, you either need to use the second suggestion, which still ought to work, or you need to forgo the automatic handling of the intermediate table and instead create an entity for it. This would allow you to apply the first suggestion. You would have the following model:
public partial class Location
{
public Location()
{
this.AttributeTypes = new List<LocationAttribute>();
}
public Location(int campusId, string code)
: this()
{
CampusId = campusId; Code = code;
}
public int Id { get; set; }
public int CampusId { get; set; }
public string Code { get; set; }
public virtual ICollection<LocationAttribute> AttributeTypes { get; set; }
}
public partial class LocationAttribute
{
[ForeignKey("LocationId")]
public Location Location { get; set; }
public int LocationId { get; set; }
public int AttributeTypeId { get; set; }
}
public partial class AttributeType
{
public int AttributeTypeId { get; set; }
public string AttributeTypeName { get; set; }
}
With this approach you do lose functionality since you can't navigate from a Location to an AttributeType without making an intermediate lookup. If you really want to do that, you need to control the entity state explicitly instead. (Doing that is not so straightforward when you want to use a generic repository, which is why I've focused on this approach instead.)
Thank you all for your suggestions.
I have to get rid of my generic repository here to save my context changes and do it manually as below,
private IEnumerable<int> AddLocationEntities(IEnumerable<LocationDataModel> locations)
{
var results = new List<int>();
foreach (LocationDataModel l in locations)
{
var entity = _mapper.Map<LocationDataModel, Location>(l);//you can map manually also
var AttributeCode = l.AssignedAttributes.FirstOrDefault().AttributeTypeId;
using (MyContext c = new MyContext())
{
var attr = c.AttributeTypes.Where(a => a.Id == AttributeTypeId ).ToList();
entity.AttributeTypes = attr;
c.Locations.Add(entity);
c.SaveChanges();
var locid = entity.Id;
results.Add(locid);
}
}
return results;
}
In the else statement of yourUpsert you should add
context.TEntity.Attach(entity);

Generic extension method for automapper

public abstract class Entity : IEntity
{
[Key]
public virtual int Id { get; set; }
}
public class City:Entity
{
public string Code { get; set; }
}
public class BaseViewModel:IBaseViewModel
{
public int Id { get; set; }
}
public class CityModel:BaseViewModel
{
public string Code { get; set; }
}
my domain and view classes...
and
for mapping extension
public static TModel ToModel<TModel,TEntity>(this TEntity entity)
where TModel:IBaseViewModel where TEntity:IEntity
{
return Mapper.Map<TEntity, TModel>(entity);
}
and i am using like below
City city = GetCity(Id);
CityModel model = f.ToModel<CityModel, City>();
but its long
can i write it like below?
City city = GetCity(Id);
CityModel model = f.ToModel();
is that possible?
Instead of jumping through all of those hoops, why not just use:
public static TDestination ToModel<TDestination>(this object source)
{
return Mapper.Map<TDestination>(source);
}
No because the 1st generic argument can't be implicitly inferred.
I would do this
public static TModel ToModel<TModel>(this IEntity entity) where TModel:IBaseViewModel
{
return (TModel)Mapper.Map(entity, entity.GetType(), typeof(TModel));
}
Then the code is still shorted than it was:
var city = GetCity(Id);
var model = city.ToModel<CityModel>();
Put the extension method on IEntity as a member method. Then you have to pass only one type.

Categories

Resources