I have many blocks of code that look like the following:
modelBuilder
.Entity<Foo>()
.Property(t => t.X)
.IsRequired()
.HasMaxLength(60)
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute("IX_X_Y", 1) { IsUnique = true }));
modelBuilder
.Entity<Foo>()
.Property(t => t.Y)
.IsRequired()
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute("IX_X_Y", 2) { IsUnique = true }));
This block is telling EF, through fluent API, to create a unique index with columns X and Y together, on table Foo.
Another block of code just like that would be this, with columns R and S on table Bar:
modelBuilder
.Entity<Bar>()
.Property(t => t.R)
.IsRequired()
.HasMaxLength(60)
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute("IX_R_S", 1) { IsUnique = true }));
modelBuilder
.Entity<Bar>()
.Property(t => t.S)
.IsRequired()
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute("IX_R_S", 2) { IsUnique = true }));
I want to refactor this, so that it ends up looking something like:
CreateCompositeUnique<Foo>(modelBuilder, "IX_X_Y", t => new {t.X, t.Y});
CreateCompositeUnique<Bar>(modelBuilder, "IX_R_S", t => new {t.R, t.S});
I was thinking of something like this:
private void CreateCompositeUnique<T>(DbModelBuilder modelBuilder, string indexName, List<Expression<Func<T, byte[]>>> properties)
{
for (int i = 0; i < properties.Count; i++)
{
modelBuilder
.Entity<typeof(T)>()
.Property(properties[i])
.IsRequired()
.HasMaxLength(60) // --only when propery is string
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute(indexName, i) { IsUnique = true }));
}
}
But I have some questions:
Is this a good idea?
How do I know if the property is string?
What is the best way to pass the parameters?
How do I access "T"? I'm getting a compile error at .Entity<typeof(T)>
Following Gert Arnold's advice, I created a extension method. Actually they are two extension methods (why? autoexplicative, see code comments)
public static class ExtensionMethods
{
public static StringPropertyConfiguration HasIndex(this StringPropertyConfiguration config, string indexName, int i)
{
return config.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation((new IndexAttribute(indexName, i) { IsUnique = true })));
}
public static PrimitivePropertyConfiguration HasIndex(this PrimitivePropertyConfiguration config, string indexName, int i)
{
return config.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation((new IndexAttribute(indexName, i) { IsUnique = true })));
}
}
And usage is like this:
modelBuilder
.Entity<Foo>()
.Property(t => t.X)
.IsRequired()
.HasMaxLength(60)
.HasIndex("IX_X_Y", 1); // <-- here (X is string)
modelBuilder
.Entity<Foo>()
.Property(t => t.Y)
.IsRequired()
.HasIndex("IX_X_Y", 2); // <-- and here (Y is a primitive)
Related
I have this class:
public class EntityClass
{
public bool IsExempt { get; set; }
public bool IsOverdue { get; set; }
public SourceEnum Status { get; set; }
}
And these two enums:
public enum SourceEnum
{
NotSet = 0,
InProgress,
Submitted,
AssessmentComplete,
Complete
}
[Flags]
public enum DestinationEnum
{
None = 0,
[Description("Exempt")]
Exempt = 1,
[Description("Unset")]
Unset = 2,
[Description("Overdue")]
Overdue = 3,
[Description("In Progress")]
InProgress = 4,
[Description("Submitted")]
Submitted = 5,
[Description("Conf. Pending")]
ConfirmationPending = 6,
[Description("Complete")]
Completed = 7
}
I want to map from EntityClass to DestinationEnum, I tried it this way:
var config = new MapperConfiguration(cfg => cfg.CreateMap<EntityClass, DestinationEnum>()
.ForMember(dest => dest, opt => opt.MapFrom(source => source.IsExempt ? DestinationEnum.Exempt : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.IsOverdue ? DestinationEnum.Overdue : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.Status == SourceEnum.InProgress ? DestinationEnum.InProgress : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.Status == SourceEnum.Complete ? DestinationEnum.Completed : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.Status == SourceEnum.AssessmentComplete ? DestinationEnum.ConfirmationPending : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.Status == SourceEnum.Submitted ? DestinationEnum.Submitted : DestinationEnum.None))
.ForMember(dest => dest, opt => opt.MapFrom(source => source.Status == SourceEnum.NotSet ? DestinationEnum.Unset : DestinationEnum.None)));
var mapper = config.CreateMapper();
var entityClassObj = new EntityClass { IsExempt = true, IsOverdue = true, Status = SourceEnum.InProgress };
var result = mapper.Map<DestinationEnum>(entityClassObj);
When I run my code, it returns:
Custom configuration for members is only supported for top-level
individual members on a type.
I tried the solution from here but unfortunately the IncludeMembers method is not available. I am using AutoMapper 6.0.2.
As #Lucian mentioned, you need to implement the Custom Type Converter for your scenario.
Solution 1
public class EntityClassToDestinationEnumConverter : ITypeConverter<EntityClass, DestinationEnum>
{
public DestinationEnum Convert(EntityClass src, DestinationEnum dest, ResolutionContext context)
{
if (src.IsExempt)
return DestinationEnum.Exempt;
if (src.IsOverdue)
return DestinationEnum.Overdue;
switch (src.Status)
{
case SourceEnum.InProgress:
return DestinationEnum.InProgress;
case SourceEnum.Complete:
return DestinationEnum.Completed;
case SourceEnum.AssessmentComplete:
return DestinationEnum.ConfirmationPending;
case SourceEnum.Submitted:
return DestinationEnum.Submitted;
case SourceEnum.NotSet:
return DestinationEnum.Unset;
}
return DestinationEnum.None;
}
}
Your Mapping Profile or MapperConfiguration to map from EntityClass to DestinationEnum.
MapperConfiguration _config = new MapperConfiguration(config =>
{
config.CreateMap<EntityClass, DestinationEnum>()
.ConvertUsing<EntityClassToDestinationEnumConverter>();
});
Solution 2
Or writing a method for the logic without a custom type resolver class.
public static class Helpers
{
public static DestinationEnum ConvertFromEntityClassToDestinationEnum(EntityClass src)
{
if (src.IsExempt)
return DestinationEnum.Exempt;
if (src.IsOverdue)
return DestinationEnum.Overdue;
switch (src.Status)
{
case SourceEnum.InProgress:
return DestinationEnum.InProgress;
case SourceEnum.Complete:
return DestinationEnum.Completed;
case SourceEnum.AssessmentComplete:
return DestinationEnum.ConfirmationPending;
case SourceEnum.Submitted:
return DestinationEnum.Submitted;
case SourceEnum.NotSet:
return DestinationEnum.Unset;
}
return DestinationEnum.None;
}
}
MapperConfiguration _config = new MapperConfiguration(config =>
{
config.CreateMap<EntityClass, DestinationEnum>()
.ConvertUsing(src => Helpers.ConvertFromEntityClassToDestinationEnum(src));
});
Demo Solution 1 & 2 # .NET Fiddle
TargetFramework: netstandard2.0
EntityFrameworkCore: 2.2.6
I have the following code in OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SlotOrder>(entity =>
{
entity.Property(e => e.Id).HasConversion(
v => v.ToString(),
v => new Guid(v));
});
modelBuilder.Entity<SlotOrderDetail>(entity =>
{
entity.Property(e => e.Id).HasConversion(
v => v.ToString(),
v => new Guid(v));
entity.HasOne<SlotOrder>()
.WithMany()
.HasForeignKey(c => c.SlotOrderId);
});
}
I do not use navigation properties and need to load all relationships of a particular entity in SaveChangesAsync. In my case if the entity is SlotOrder I need to determine that it has a child entity SlotOrderDetail:
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ChangeTracker.DetectChanges();
var utcNow = DateTime.UtcNow;
var added = ChangeTracker.Entries()
.Where(t => t.State == EntityState.Added)
.Select(t => t.Entity)
.ToList();
added.ForEach(entity =>
{
if (entity is IAuditable auditable)
{
auditable.CreatedAt = utcNow;
auditable.UpdatedAt = utcNow;
}
// var relationships = ...
});
return base.SaveChangesAsync(cancellationToken);
}
Any clue how to do that?
The relationship metadata is provided by IForeignKey interface.
Given an IEntityType, there are two methods that you can use to obtain information for entity relationships - GetForeignKeys which returns the relationships where the entity is the dependent, and GetReferencingForeignKeys which return the relationships where the entity is the principal.
In your case, don't select the .Entity property, use the EntityEntry which gives you access to the IEntityType via Metadata property, e.g.
var addedEntries = ChangeTracker.Entries()
.Where(t => t.State == EntityState.Added)
.ToList();
addedEntries.ForEach(entry =>
{
if (entry.Entity is IAuditable auditable)
{
auditable.CreatedAt = utcNow;
auditable.UpdatedAt = utcNow;
}
var foreignKeys = entry.Metadata.GetForeignKeys();
var referencingForeignKeys = entry.Metadata.GetReferencingForeignKeys();
});
I don't think it's possible to do that but I just got another idea, something like this. Create new function to do SaveChanges then load all.
In any class you create you like.
public IQueryable<T> CommitLoad<T>() where T : class
{
db.SaveChanges();
var list = db.Set<T>().AsQueryable();
var key = db.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties.FirstOrDefault();
var foreignkeys = key.GetContainingPrimaryKey().GetReferencingForeignKeys();
if (foreignkeys.Count() > 0)
{
foreach (var item in foreignkeys)
list = list.Include<T>(item.DeclaringEntityType.DisplayName());
}
return list;
}
Any class or page
public IQueryable<SlotOrder> GetTest()
{
//Save record to table
//After saving record, savechanges + load all
var list = CommitLoad<SlotOrder>();
return list;
}
Here is result screenshot
I want the null value during mapping DTO to DBO model to be ignored. This is the code:
DTO / DBO models have both property named items:
public virtual ICollection<price_list_item> items { get; set; }
DBO constructor:
public price_list()
{
this.items = new List<price_list_item>();
}
DTO constructor has no propert initialization
public price_list()
{
}
AutoMapper Profile:
this.CreateMap<DTO.price_list, DBO.price_list>()
.ForMember(m => m.id, src => src.Ignore())
.ForMember(m => m.currency_id, src => src.MapFrom(f => f.currency))
.ForMember(dest => dest.items, opt => opt.Condition(src => (src.items != null)))
API Controller:
[HttpPut]
[Route("{id:long}")]
public async Task<DTO.price_list> UpdateOneAsync(long id, [FromBody]DTO.price_list payload)
{
if (payload == null)
{
throw new ArgumentNullException("payload");
}
Console.WriteLine(payload.items == null);
var _entity = await this.IDataRepository.price_lists
.Where(w => w.id == id)
.Include(i => i.items)
.FirstOrDefaultAsync();
if (_entity == null)
{
NotFound();
return null;
}
Console.WriteLine(_entity.items.Count);
// map fields to existing model
this.IMapper.Map<DTO.price_list, DBO.price_list>(payload, _entity);
Console.WriteLine(_entity.items.Count);
When I send to API a JSON without any sign of 'items' property, Console output is:
True
1200 // price list in dbo has 1200 items
0 // here I need to have still 1200 items
What am I doing wrong? Why the condition is not respected and items property is not 'skiped' ?
Thanks
Lucian thanks, PreCondition solved the problem. This is working code:
this.CreateMap<DTO.price_list, DBO.price_list>()
.ForMember(m => m.id, src => src.Ignore())
.ForMember(m => m.currency_id, src => src.MapFrom(f => f.currency))
.ForMember(dest => dest.items, opt => opt.PreCondition(src => (src.items != null)))
I'm trying to map a viewmodel to an entity from EF. I have gotten so far that I can map all properties from the viewmodel to the entity but I am trying to ignore all properties that is null (the data in the viewmodel comes from a form and I don't want to put null as the pk for example). I want to be able to do something like this:
IUserDetails objUserDetails = GetDataFromForm();
var user = db.Users.FirstOrDefault();
user.UpdateUser(objUserDetails);
This is what I have come up with so far:
public static class UserExtensions
{
public static void UpdateUser(this IUser user, IUserDetails userDetails)
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<IUserDetails, IUser>()
.UseDestinationValue()
.IgnoreNullValues()
.ForMember(dest => dest.AddressLine1, opt => opt.MapFrom(src => src.Address1))
.ForMember(dest => dest.AddressLine2, opt => opt.MapFrom(src => src.Address2))
.ForMember(dest => dest.LName, opt => opt.MapFrom(src => src.LastName))
.ForMember(dest => dest.UserGUID, opt => opt.MapFrom(src => src.GUID));
});
var mapper = config.CreateMapper();
mapper.Map<IUserDetails, IUser>(userDetails, user);
}
private static IMappingExpression<TSource, TDest> UseDestinationValue<TSource, TDest>(this IMappingExpression<TSource, TDest> expression)
{
expression.ForAllMembers(opt => opt.UseDestinationValue());
return expression;
}
private static IMappingExpression<TSource, TDest> IgnoreNullValues<TSource, TDest>(this IMappingExpression<TSource, TDest> expression)
{
expression.ForAllMembers(opt => opt.Condition((src, dest, srcVal, destVal, c) => { Debugger.Break(); return srcVal != null; }));
return expression;
}
}
As you can see I have put a breakpoint in IgnoreNullValues() and when I debug the code I can see that srcVal is never anything else than null, however all the other values looks good. What am I missing?
Update:
Apparently this is due to a bug in 5.1.1, downgrading to 5.0.2 made it all work.
I'm testing a PUT with two string:
company.CurrencyCode = request.CurrencyCode ?? company.CurrencyCode;
company.CountryIso2 = request.Country ?? company.CountryIso2;
and I tried with a rule like:
public UpdateCompanyValidator()
{
RuleSet(ApplyTo.Put, () =>
{
RuleFor(r => r.CountryIso2)
.Length(2)
.When(x => !x.Equals(null));
RuleFor(r => r.CurrencyCode)
.Length(3)
.When(x => !x.Equals(null));
});
}
as I don't mind to get a null on those properties, but I would like to test the Length when the property is not a null.
What is the best way to apply rules when a property is nullable and we just want to test if it's not null?
One of the ways would be:
public class ModelValidation : AbstractValidator<Model>
{
public ModelValidation()
{
RuleFor(x => x.Country).Must(x => x == null || x.Length >= 2);
}
}
I prefer the following syntax:
When(m => m.CountryIso2 != null,
() => {
RuleFor(m => m.CountryIso2)
.Length(2);
);
Best syntax for me:
RuleFor(t => t.DocumentName)
.NotEmpty()
.WithMessage("message")
.DependentRules(() =>
{
RuleFor(d2 => d2.DocumentName).MaximumLength(200)
.WithMessage(string.Format(stringLocalizer[""message""], 200));
});