CreateMap<T,U> for Nullable<TMember> to TMember - c#

Lets say I have an internal EF model:
public class EFModel
{
public int key { get; set; }
public string Name { get; set; }
public int? OptionalId { get; set;}
}
And my external facing model with the same data types except that none are Nullable:
public class Extmodel
{
public int key { get; set; }
public string Name { get; set; }
public int OptionalId { get; set; }
}
In general I'd have to go through CreateMap and explicitly cast:
CreateMap<EfModel,ExtModel>
.ForMember(a => a.OptionalId, b => b.MapFrom(c => (int)c.OptionalId))
.ReverseMap();
But imagine having a very large database, there should be some way to speed this up. I tried to a helper function that works on 'System.Reflection' and generics to sort this out for me.
public void MapWrap<T, U>() where T : new()
{
var newT = new T();
var map = CreateMap<T, U>();
foreach (var member in newT.GetType().GetProperties())
{
Type memberType = member.PropertyType;
var nullable = Nullable.GetUnderlyingType(memberType) == null;
var notString = memberType != typeof(string);
if (nullable && notString)
{
map.ForMember(dest => dest.GetType().GetMethod(member.Name),
src => src.MapFrom(
dt => Convert.ChangeType(dt.GetType().GetMethod(member.Name), Nullable.GetUnderlyingType(memberType))));
}
}
map.ReverseMap();
}
Assumptions:
All internal and external names are identical
All castings are strictly Nullable to T
I am getting an error 'Custom configuration for members is only supported for top-level individual members on a type.' Am I thinking about this all wrong?

Related

Reflection, get explicit interface property 'backing field' name

Is there a way to get the name of an explicit interface property backing field?
For example, for:
public interface I
{
string PPPP { get; }
}
public class C: I
{
private string _other_field = default!; // random private field, just to fill.
public string S => _s_backing; // random property, just to fill.
private string _s_backing = default!;
string I.PPPP => _s_backing; // <--- looking for this one!
}
For property PPPP I'm looking to figure up the string "_s_backing".
I mean. Is there a way to create this helper:
Helpers.DoSomeReflectionMagic( typeof(C), "PPPP" )
// I expect, it returns: `_s_backing`.
What I tried: I was digging into typeof(C) properties, but I didn't find the backing field anywhere. Maybe there is no way to get it.
The underlying XY problem:
public interface ITree<T>
{
T? Parent { get; }
IEnumerable<T> Children { get; }
}
public class Category: ITree<Category>
{
public string Name { get; set; }
public Category? MyParentCategory { get; set; }
public IEnumerable<Category> MySubCategories { get; set; }
// Implementing interface
Category? ITree<Category>.Parent => MyParentCategory;;
IEnumerable<Category> ITree<Category>.Children => MySubCategories;
}
// dbcontext with fluent api to configure model blah blah
public static class DbContextTreeExtensions
{
public static IEnumerable<T> GetRoots<T>(this MyDbContext ctx)
{
var backingfieldname = // <-- The Y problem
Helpers
.DoSomeReflectionMagic(typeof(T), "Parent");
return
ctx
.Set<T>()
.Where(q => EF.Property<T?>(q, backingfieldname) == null)
.AsEnumerable();
}
}
I would like to do:
var ctx = MyDbContextFactory.NewContext();
var mainCategories = ctx.GetRoots<Category>();

Sort according to a custom attribute

Please Consider this code:
public class MyClass
{
[CustomAttributes.GridColumn(1)]
public string Code { get; set; }
[CustomAttributes.GridColumn(3)]
public string Name { get; set; }
[CustomAttributes.GridColumn(2)]
public DateTime? ProductionDate { get; set; }
public DateTime? ProductionExpiredDate { get; set; }
[CustomAttributes.GridColumn(4)]
public int ProductOwner { get; set; }
}
I want to get a dictionary for all properties that have CustomAttributes.GridColumn and sort them by the number in GridColumn attribute and type of them like this:
PropertyName Type
---------------------------------
Code string
ProductionDate DateTime?
Name string
ProductOwner int
How I can do this?
Thanks
Something like this ought to work:
private IDictionary<string, Type> GetProperties<T>()
{
var type = typeof(T);
return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Select(p => new { Property = p, Attribute = p.GetCustomAttribute<CustomAttributes.GridColumnAttribute>() })
.Where(p => p.Attribute != null)
.OrderBy(p => p.Attribute.Index)
.ToDictionary(p => p.Property.Name, p => p.Property.PropertyType);
}
It first gets all of the public properties, creates an object which contains the property and the attribute, filters the list to only include properties where the attribute exists, sorts by the attribute index, and finally converts it into a dictionary.
I'm assuming the attribute is defined similar to this:
public class GridColumnAttribute : System.Attribute
{
public GridColumnAttribute(int index)
{
this.Index = index;
}
public int Index { get; set; }
}
P.S. GetCustomAttribute<T>() is an extension method that lives in System.Reflection.CustomAttributeExtensions so make sure you include a using System.Reflection;
Try it online

Map nested elements to related lists using AutoMapper

I have two sets of objects
Objects that I use in C# client application:
public class EmployeeClient
{
public int Id { get; set; }
public int DepartmentId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string MiddleName { get; set; }
}
public class DepartmentClient
{
public int Id { get; set; }
public string Name { get; set; }
}
public class OrganizationClient
{
public int Id { get; set; }
public string Name { get; set; }
public List<DepartmentClient> Departments { get; set; }
public List<EmployeeClient> Employees { get; set; }
}
And DTOs:
public class EmployeeDto
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string MiddleName { get; set; }
}
public class DepartmentDto
{
public int Id { get; set; }
public string Name { get; set; }
public List<EmployeeDto> Employees { get; set; }
}
public class OrganizationDto
{
public int Id { get; set; }
public string Name { get; set; }
public List<DepartmentDto> Departments { get; set; }
}
I use AutoMapper and I need to configure mapping Client -> DTOs and DTOs -> Client.
I implemented mapping DTOs->Client like this:
public class DtoToClientMappingProfile: Profile
{
public DtoToClientMappingProfile()
{
CreateMap<EmployeeDto, EmployeeClient>();
CreateMap<DepartmentDto, DepartmentClient>();
CreateMap<OrganizationDto, OrganizationClient>()
.ForMember(dest => dest.Employees, opt => opt.ResolveUsing(src => src.Departments.SelectMany(d => d.Employees)))
.AfterMap(AfterMap);
}
private void AfterMap(OrganizationDto dto, OrganizationClient client)
{
foreach (var department in dto.Departments)
{
foreach (var employee in department.Employees)
{
var clientEmployee = client.Employees.First(e => e.Id == employee.Id);
clientEmployee.DepartmentId = department.Id;
}
}
}
}
It is not universal solution, but is works for me.
I've found only one option how mapping Client->DTOs could be implemented:
public class ClientToDtosMappingProfile : Profile
{
public ClientToDtosMappingProfile()
{
CreateMap<EmployeeClient, EmployeeDto>();
CreateMap<DepartmentClient, DepartmentDto>();
CreateMap<OrganizationClient, OrganizationDto>()
.AfterMap(AfterMap);
}
private void AfterMap(OrganizationClient client, OrganizationDto dto)
{
foreach (var employee in client.Employees)
{
var departmentDto = dto.Departments.First(d => d.Id == employee.DepartmentId);
if (departmentDto.Employees == null)
{
departmentDto.Employees = new List<EmployeeDto>();
}
var configuration = (IConfigurationProvider)new MapperConfiguration(cfg =>
{
cfg.AddProfiles(typeof(ClientToDtosMappingProfile));
});
var mapper = (IMapper)new Mapper(configuration);
var employeeDto = mapper.Map<EmployeeDto>(employee);
departmentDto.Employees.Add(employeeDto);
}
}
}
It works, but I do not like this solution because I should create instance of new Mapper every time I map objects. In my real code Employee has a lot of nested elements and mapping is configured in multiple profiles.
Any ideas how it could be implemented better?
I made my code a bit better using ResolutionContext. It allows not to create mappers in AfterMap function.
DtoToClientMappingProfile:
public class DtoToClientMappingProfile: Profile
{
public DtoToClientMappingProfile()
{
CreateMap<EmployeeDto, EmployeeClient>();
CreateMap<DepartmentDto, DepartmentClient>();
CreateMap<OrganizationDto, OrganizationClient>()
.ForMember(dest => dest.Employees, opt => opt.Ignore())
.AfterMap(AfterMap);
}
private void AfterMap(OrganizationDto dto, OrganizationClient client, ResolutionContext resolutionContext)
{
if (dto.Departments == null)
{
return;
}
client.Departments = new List<DepartmentClient>();
foreach (var department in dto.Departments)
{
var departmentClient = resolutionContext.Mapper.Map<DepartmentClient>(department);
client.Departments.Add(departmentClient);
if (department.Employees == null)
{
continue;
}
if (client.Employees == null)
{
client.Employees = new List<EmployeeClient>();
}
foreach (var employee in department.Employees)
{
var employeeClient = resolutionContext.Mapper.Map<EmployeeClient>(employee);
employeeClient.DepartmentId = department.Id;
client.Employees.Add(employeeClient);
}
}
}
ClientToDtosMappingProfile:
public class ClientToDtosMappingProfile : Profile
{
public ClientToDtosMappingProfile()
{
CreateMap<EmployeeClient, EmployeeDto>();
CreateMap<DepartmentClient, DepartmentDto>();
CreateMap<OrganizationClient, OrganizationDto>()
.AfterMap(AfterMap);
}
private void AfterMap(OrganizationClient client, OrganizationDto dto, ResolutionContext resolutionContext)
{
if (client.Employees == null)
{
return;
}
foreach (var employee in client.Employees)
{
var departmentDto = dto.Departments.First(d => d.Id == employee.DepartmentId);
if (departmentDto.Employees == null)
{
departmentDto.Employees = new List<EmployeeDto>();
}
var employeeDto = resolutionContext.Mapper.Map<EmployeeDto>(employee);
departmentDto.Employees.Add(employeeDto);
}
}
}
If you call AssertConfigurationIsValid, AM will complain about what it doesn't know how to map.
The problem seems to be that you don't have the information needed to fill the destination object in the source object.
You will need to add a resolver for each property AM complains about, like the ResolveUsing you already have, for example.
You also need to pass the extra information that's needed.
The result may not look good eventually because AM cannot rely on uniform objects to do its job, you have to tell it what to do.
Another way to go about it is to do the high level mapping in your own code and rely on AM only when the mapping is simple enough so AM can do it by itself. The more you customize AM, the less value you get from it.

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.

How to exclude all virtual properties from classes inheritted from ClassMapper<T> by default?

I have a lot of POCO classes that contain several virtual properties each. Something like this:
public class Policy
{
public int Id { get; set; }
public int EntityId { get; set; }
public int ProgramId { get; set; }
public string PolicyNumber { get; set; }
public DateTime EffectiveDate { get; set; }
public DateTime ExpirationDate { get; set; }
public virtual Entity Entity{ get; set; }
public virtual Program Program { get; set; }
public virtual ICollection<Transaction> Transactions { get; set; }
}
To make Dapper.Extensions work, I need to write a mapping for each of these classes, which is fine. My problem is, if there are any virtual properties inside a class, they need to be explicitly marked as ignored, which I always forget to do.
public sealed class PolicyMapper : BaseMapper<Policy>
{
public PolicyMapper()
{
Map(p => p.Entity).Ignore();
Map(p => p.Program).Ignore();
Map(p => p.Transactions).Ignore();
AutoMap();
}
}
What would be great for me, if the Dapper.Extensions library will automatically exclude virtual properties, if any, when mapped to the POCO class. There is an extension for Automapper that does something similar (link). Is there a way to do that for Dapper.Extensions library? Possibly something like this:
public sealed class PolicyMapper : BaseMapper<Policy>
{
public PolicyMapper()
{
IgnoreAllVirtual();
AutoMap();
}
}
I found my own solution. Since all my mapping classes derive from BaseMapper class, I decided to override AutoMap() method that will exclude virtual properties:
public class BaseMapper<T> : ClassMapper<T> where T : BaseClass
{
public BaseMapper()
{
}
protected override void AutoMap()
{
CustomAutoMap(null);
}
private void CustomAutoMap(Func<Type, PropertyInfo, bool> canMap)
{
Type type = typeof(T);
bool hasDefinedKey = Properties.Any(p => p.KeyType != KeyType.NotAKey);
PropertyMap keyMap = null;
foreach (var propertyInfo in type.GetProperties())
{
// Exclude virtual properties
if (propertyInfo.GetGetMethod().IsVirtual)
{
continue;
}
if (Properties.Any(p => p.Name.Equals(propertyInfo.Name, StringComparison.InvariantCultureIgnoreCase)))
{
continue;
}
if ((canMap != null && !canMap(type, propertyInfo)))
{
continue;
}
PropertyMap map = Map(propertyInfo);
if (!hasDefinedKey)
{
if (string.Equals(map.PropertyInfo.Name, "id", StringComparison.InvariantCultureIgnoreCase))
{
keyMap = map;
}
if (keyMap == null && map.PropertyInfo.Name.EndsWith("id", true, CultureInfo.InvariantCulture))
{
keyMap = map;
}
}
}
if (keyMap != null)
{
keyMap.Key(PropertyTypeKeyTypeMapping.ContainsKey(keyMap.PropertyInfo.PropertyType)
? PropertyTypeKeyTypeMapping[keyMap.PropertyInfo.PropertyType]
: KeyType.Assigned);
}
}
}
}

Categories

Resources