How do you centralise ASP.NET Core Web API attribute validation on multiple DTOs with similar properties? - asp.net-core

Is there a way to centralise the model validation of the same property name across multiple DTOs?
For example, if I have the following classes to be used as the request body in a Web API action.
public class RegisterRequest
{
[Required]
[EmailAddress]
public string EmailAddress { get; set; } = null!;
[Required]
[MinLength(8)]
[RegularExpression(UserSettings.PasswordRegex)]
public string Password { get; set; } = null!;
[Required]
[MaxLength(100)]
public string DisplayName { get; set; } = null!;
}
public class UserProfileRequest
{
[Required]
public int UserId { get; set; }
[Required]
[MaxLength(100)]
public string DisplayName { get; set; } = null!;
[Range(3, 3)]
public string? CCN3 { get; set; }
}
Can I centralise the attribute validation on DisplayName, duplicating the attributes goes against single responsibility principle. I believe I could achieve the centralised validation using an IFilterFactory and dropping the usage of attributes.

I opted to use a custom ActionFilterAttribute to achieve centralisation of the validation. The example below is for validating the country code (CCN3).
CountryCodeValidationAttribute.cs - custom attribute to be applied to properties (contains no logic)
[AttributeUsage(AttributeTargets.Property)]
public class CountryCodeValidationAttribute : Attribute
{
}
CountryCodeValidationActionFilter.cs - custom action filter that supports dependency injection and looks for the custom attribute on the properties. In my case I'm returning the standard invalid model bad request response.
public class CountryCodeValidationActionFilter : ActionFilterAttribute
{
private readonly ICountryService countryService;
private readonly IOptions<ApiBehaviorOptions> apiBehaviorOptions;
public CountryCodeValidationActionFilter(
ICountryService countryService,
IOptions<ApiBehaviorOptions> apiBehaviorOptions)
{
this.countryService = countryService;
this.apiBehaviorOptions = apiBehaviorOptions;
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var actionArguments = context.ActionArguments;
foreach (var actionArgument in actionArguments)
{
if (actionArgument.Value == null) continue;
var propertiesWithAttributes = actionArgument.Value
.GetType()
.GetProperties()
.Where(x => x.GetCustomAttributes(true).Any(y => y.GetType() == typeof(CountryCodeValidationAttribute)))
.ToList();
foreach (var property in propertiesWithAttributes)
{
var value = property.GetValue(actionArgument.Value)?.ToString();
if (value != null && await countryService.GetCountryAsync(value) != null) await next();
else
{
context.ModelState.AddModelError(property.Name, "Must be a valid country code");
context.Result = apiBehaviorOptions.Value.InvalidModelStateResponseFactory(context);
}
}
}
await base.OnActionExecutionAsync(context, next);
}
}
Program.cs - register the custom action filter.
builder.Services.AddMvc(options =>
{
options.Filters.Add(typeof(CountryCodeValidationActionFilter));
});
UserProfile.cs - apply the [CountryCodeValidation] attribute to the CountryCode property.
public class UserProfile
{
[Required]
[MaxLength(100)]
public string DisplayName { get; set; } = null!;
[CountryCodeValidation]
public string? CountryCode { get; set; }
}
I can take this same approach and apply it to the DisplayName property to create a centralised validation for it 👍.

Related

how to send array to API which contains image and other data in .net core

When I am passing a single object like below then it is working as per below image
[HttpPost]
public async Task<ActionResult> Post([FromForm] MyModel Details)
{
}
but when I am passing the List of the object to API then it is not working. option to upload a file is not visible. and if I entered any values in the array then also I am getting count 0 for details.
[HttpPost]
public async Task<ActionResult> Post([FromForm] List<MyModel> Details)
{}
I want to pass the List of images and descriptions to API. How can I achieve it?
Thanks in advance!
You need custom model binding for the list model . Here is a similar demo:
custom model binding code:
public class MetadataValueModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var values = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (values.Length == 0)
return Task.CompletedTask;
var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
var deserialized = JsonSerializer.Deserialize(values.FirstValue, bindingContext.ModelType, options);
bindingContext.Result = ModelBindingResult.Success(deserialized);
return Task.CompletedTask;
}
}
Add the model binder to the model class:
public class MasterDTO
{
public string Comments { get; set; }
public IFormFile File { get; set; }
public List<DetailDTO> Details { get; set; }
public MasterDTO()
{
this.Details = new List<DetailDTO>();
}
}
[ModelBinder(BinderType = typeof(MetadataValueModelBinder))]
public class DetailDTO
{
public Int64 ElementId { get; set; }
public double LowerLimit { get; set; }
public double HigherLimit { get; set; }
public string Status { get; set; }
public string UserAuthorization { get; set; }
public DateTime? AutorizationDate { get; set; }
}
controller/action
[HttpPost]
public async Task<IActionResult> CreateProjectLimit([FromForm] MasterDTO masterDto)
{
//...
return Ok();
}
You can just use postman to pass the list of images and Descriptions to API
Below is the right answer. we can use Postman to pass images in the array as shown below.

How to define a System.Security.Claims.ClaimsPrincipal as a request argument in gRPC?

I would like to build a authorization service using gRPC under .Net Code. In order to do that, I need to pass a System.Security.Claims.ClaimsPrincipal object as a request argument from caller to the server so the server can use it to authorize the caller. But I don't know how to that - how can I define a .proto for a class that is a standard library. What am I supposed to do?
I'm doing the same, using protobuf-net grpc libraries.
As many of the Identity/Security classes (if you are using them) are from Microsoft, you'll need to expose their members for serialization; you can use:
RuntimeTypeModel.Default.Add(typeof(SignInResult), false).Add(
nameof(SignInResult.Succeeded),
nameof(SignInResult.IsLockedOut),
nameof(SignInResult.IsNotAllowed),
nameof(SignInResult.RequiresTwoFactor)
);
and list the members that need to be exposed over gRpc.
As for ClaimsPrincipal, specifically, that is what I'm currently trying to implement. For Claims, i'm using a surrogate class:
RuntimeTypeModel.Default.Add(typeof(Claim), true).SetSurrogate(typeof(ClaimSurrogate));
public class ClaimSurrogate
{
[DataMember, ProtoMember(1)]
public string Type { get; set; }
[DataMember, ProtoMember(2)]
public ClaimsIdentity Subject { get; set; }
[DataMember, ProtoMember(3)]
public IDictionary<string, string> Properties { get; set; }
[DataMember, ProtoMember(4)]
public string OriginalIssuer { get; set; }
[DataMember, ProtoMember(5)]
public string Issuer { get; set; }
[DataMember, ProtoMember(6)]
public string ValueType { get; set; }
[DataMember, ProtoMember(7)]
public string Value { get; set; }
public static implicit operator ClaimSurrogate(Claim claim)
{
if (claim == null)
return null;
return new ClaimSurrogate()
{
Type = claim.Type,
Subject = claim.Subject,
Properties = claim.Properties,
OriginalIssuer = claim.OriginalIssuer,
Issuer = claim.Issuer,
ValueType = claim.ValueType,
Value = claim.Value
};
}
public static implicit operator Claim(ClaimSurrogate surrogate)
{
if (surrogate == null)
return null;
return new Claim(surrogate.Type, surrogate.Value, surrogate.ValueType, surrogate.Issuer, surrogate.OriginalIssuer, surrogate.Subject);
}
}
And I'm assuming that ClaimsPrincipal can be done the same way, but, I'm having trouble with it. That is how I came across your question...
Actually, by, trying to provide an answer...Literally, I just realized what I overlooked, I need to also set up a surrogate for the ClaimsIdentity
So far, I've needed surrogates for 'third' party classes that have get; only properties. ClaimsPrincipal has these types of properties, and so does ClaimsIdentity (as does Claim). I'll update/comment if ClaimsIdentitySurrogate does the trick
Updates:
Yes, it can be done. Surrogates, like the example above, will be needed for ClaimsIdentity and IIdentity. These classes are used as members/properties within ClaimsPrincipal.
ClaimsIdentity: you can mix up the SetSurrogate and the Add(nameof(...)) as it has get onlies and get/sets (get/sets go in the Add portion). Do not include the Actor in the ClaimsIdentity surrogate as it will create a never ending loop in your service's startup. If you do include it, make sure it is not a DataMember/Protomember. And (private) set it in the surrogate operator. Same same with Claims.
Essentially, any surrogates with members that reference the parent class, or that of a another type with a surrogate that references this parent type, will create a circular reference and error out your service on startup.
IIdentity: This is a simple one, just
RuntimeTypeModel.Default.Add(typeof(IIdentity), false).
Lastly (I posted this update when I thought I had it, but, amidst all the UT tests and changes, etc, I posted a bit early; after making a breaking change on the ClaimPrincipal surrogate class)....
You'll want an IIdentity dummy class that will be used in your ClaimPrincipal surrogate, instead of the IIdentity Identity {get;set;}. This dummy class should inherit from IIdentity, e.g.
[DataContract]
public class IIdentityFraud : System.Security.Principal.IIdentity
And within your surrogate's implicit operator:
IIdentityFraud identityfraud = null;
if (claimPrincipal.Identity != null)
{
identityfraud = new IIdentityFraud(claimPrincipal.Identity.AuthenticationType, claimPrincipal.Identity.Name, claimPrincipal.Identity.IsAuthenticated);
}
Updates (11/05/2021):
[DataContract]
public class ClaimsPrincipalSurrogate
{
[DataMember, ProtoMember(1)]
public IIdentityFraud Identity { get; set; }
[DataMember, ProtoMember(2)]
public IEnumerable<ClaimsIdentity> Identities { get; set; }
[DataMember, ProtoMember(3)]
public IEnumerable<Claim> Claims { get; set; }
public static implicit operator ClaimsPrincipalSurrogate(ClaimsPrincipal claimPrincipal)
{
if (claimPrincipal == null)
{
return null;
}
else
{
IIdentityFraud identityfraud = null;
if (claimPrincipal.Identity != null)
{
identityfraud = new IIdentityFraud(claimPrincipal.Identity.AuthenticationType, claimPrincipal.Identity.Name, claimPrincipal.Identity.IsAuthenticated);
}
return new ClaimsPrincipalSurrogate()
{
Identity = identityfraud, // (System.Security.Principal.IIdentity)identityfraud,
Identities = claimPrincipal.Identities,
Claims = claimPrincipal.Claims
};
}
}
public static implicit operator ClaimsPrincipal(ClaimsPrincipalSurrogate surrogate)
{
if (surrogate == null)
return null;
if (surrogate.Identities != null && surrogate.Identities.Any() == true)
{
return new ClaimsPrincipal(surrogate.Identities);
}
else if (surrogate.Identity != null)
{
return new ClaimsPrincipal(surrogate.Identity);
}
return new ClaimsPrincipal();
}
}
[DataContract]
public class ClaimsIdentitySurrogate
{
[DataMember, ProtoMember(1)]
public string AuthenticationType { get; set; }
[DataMember, ProtoMember(2)]
public string Name { get; set; }
//[DataMember, ProtoMember(3)]
//public string Label { get; set; }
[DataMember, ProtoMember(4)]
public bool IsAuthenticated { get; set; }
[DataMember, ProtoMember(5)]
public IEnumerable<Claim> Claims { get; private set; }
//[DataMember, ProtoMember(6)]
//public object BootstrapContext { get; set; }
//[DataMember, ProtoMember(7)]
public ClaimsIdentity Actor { get; private set; }
[DataMember, ProtoMember(8)]
public string RoleClaimType { get; set; }
[DataMember, ProtoMember(9)]
public string NameClaimType { get; set; }
public static implicit operator ClaimsIdentitySurrogate(ClaimsIdentity claimIdentity)
{
if (claimIdentity == null)
return null;
return new ClaimsIdentitySurrogate()
{
AuthenticationType = claimIdentity.AuthenticationType,
Name = claimIdentity.Name,
//Label = claimIdentity.Label,
IsAuthenticated = claimIdentity.IsAuthenticated,
Claims = claimIdentity.Claims,
//BootstrapContext = claimIdentity.AuthenticationType,
Actor = claimIdentity.Actor,
RoleClaimType = claimIdentity.RoleClaimType,
NameClaimType = claimIdentity.NameClaimType
};
}
public static implicit operator ClaimsIdentity(ClaimsIdentitySurrogate surrogate)
{
if (surrogate == null)
{
return null;
}
if (surrogate.Claims?.Any() == true)
{
return new ClaimsIdentity(surrogate.Claims, surrogate.AuthenticationType);
}
else
{
return new ClaimsIdentity(surrogate.AuthenticationType, surrogate.NameClaimType, surrogate.RoleClaimType);
}
}
}
[DataContract]
public class IIdentityFraud : System.Security.Principal.IIdentity
{
[DataMember, ProtoMember(1)]
public string AuthenticationType { get; private set; }
[DataMember, ProtoMember(2)]
public string Name { get; private set; }
[DataMember, ProtoMember(3)]
public bool IsAuthenticated { get; private set; }
public IIdentityFraud() { }
public IIdentityFraud(string authenticationType, string name, bool isAuthenticated)
{
this.AuthenticationType = authenticationType;
this.Name = name;
this.IsAuthenticated = isAuthenticated;
}
}
[DataContract] //don't know if this is really needed. Too involved in testing out the rest of it and have yet to come back to this.
public class IIdentitySurrogate : System.Security.Principal.IIdentity
{
[DataMember, ProtoMember(1)]
public string AuthenticationType { get; set; }
[DataMember, ProtoMember(2)]
public string Name { get; set; }
[DataMember, ProtoMember(3)]
public bool IsAuthenticated { get; set; }
public static implicit operator IIdentitySurrogate(IIdentityFraud iidentity)
{
if (iidentity == null)
return null;
return new IIdentitySurrogate()
{
AuthenticationType = iidentity.AuthenticationType,
Name = iidentity.Name,
IsAuthenticated = iidentity.IsAuthenticated
};
}
public static implicit operator IIdentityFraud(IIdentitySurrogate surrogate)
{
if (surrogate == null)
return null;
return new IIdentityFraud(surrogate.AuthenticationType, surrogate.Name, surrogate.IsAuthenticated);
}
}
More of what is executed on startups:
#region ClaimsIdentity
RuntimeTypeModel.Default.Add(typeof(ClaimsIdentity), true).Add(
nameof(ClaimsIdentity.Label),
nameof(ClaimsIdentity.BootstrapContext),
nameof(ClaimsIdentity.Actor)
).SetSurrogate(typeof(ClaimsIdentitySurrogate));
#endregion ClaimsIdentity
#region ClaimsPrincipal
RuntimeTypeModel.Default.Add(typeof(ClaimsPrincipal), true).SetSurrogate(typeof(ClaimsPrincipalSurrogate));
#endregion ClaimsPrincipal
#region IIdentity
RuntimeTypeModel.Default.Add(typeof(IIdentity), true);
#endregion IIdentity

How to update an existing entity that has a nested list of entities?

I'm trying to update an entity using entity framework but, everytime I try to do it, it raises an error saying that a nested entity the main class contains cannot be tracked.
These are my classes:
public abstract class BaseEntity
{
public int Id { get; set; }
}
public class Dashboard : BaseEntity
{
public int Order { get; set; }
public string Title { get; set; }
public bool Enabled { get; set; }
public virtual ICollection<Submenu> Submenu { get; set; }
}
public class Submenu : BaseEntity
{
public int Order { get; set; }
public bool Enabled { get; set; }
public string Title { get; set; }
public string Image { get; set; }
public string Descriptions { get; set; }
public virtual ICollection<Action> Actions { get; set; }
public int DashboardId { get; set; }
public virtual Dashboard Dashboard { get; set; }
}
public class Action : BaseEntity
{
public string Type { get; set; }
public string Label { get; set; }
public string Url { get; set; }
public string Extension { get; set; }
public virtual Submenu Submenu { get; set; }
public int SubmenuId { get; set; }
}
The one I am using to update is Dashboard, which contains the rest of the classes.
I'm trying to do it using a generic service layer and a generic repository that are defined this way:
public class GenericService<T> : IGenericService<T> where T : BaseEntity
{
private readonly IBaseRepository<T> baseRepository;
public GenericService(IBaseRepository<T> baseRepository)
{
this.baseRepository = baseRepository;
}
public async Task Update(T entity, T attachedEntity)
{
await baseRepository.Update(entity, attachedEntity);
}
}
public class BaseRepository<T> : IBaseRepository<T> where T : BaseEntity
{
private readonly PortalContext dataContext;
private DbSet<T> DbSet { get; set; }
public BaseRepository(PortalContext context)
{
dataContext = context;
DbSet = dataContext.Set<T>();
}
public async Task Update(T entity, T attachedEntity)
{
dataContext.Entry(attachedEntity).State = EntityState.Detached;
DbSet.Attach(entity);
dataContext.Entry(entity).State = EntityState.Modified;
await dataContext.SaveChangesAsync();
}
}
And, at last but no least, this is the way I am configuring everything at Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<PortalContext>(
options => options.UseSqlServer(Configuration.GetConnectionString("PortalContext"))
);
services.AddTransient(typeof(IGenericService<>), typeof(GenericService<>));
services.AddTransient(typeof(IBaseRepository<>), typeof(BaseRepository<>));
services.AddTransient<Func<string, ClaimsPrincipal, IRoleCheck>>((serviceProvider) =>
{
return (controllerName, claimsPrincipal) =>
new RoleCheck(serviceProvider.GetRequiredService<IGenericService<Dossier>>(),
serviceProvider.GetRequiredService<IGenericService<DossierTemplate>>(),
serviceProvider.GetRequiredService<IGenericService<Dashboard>>(),
controllerName, claimsPrincipal);
});
}
What the application first does is calling the RoleCheck class to retrieve and filter the required entities and, after that, the user can update them.
When I call the update function at the controller
public async Task<ActionResult<Dashboard>> Put(int id, [FromBody] Dashboard dashboard)
{
var currentDashboard = await service.Get(id);
if (currentDashboard == null)
{
return NotFound();
}
await service.Update(dashboard, currentDashboard);
return Ok();
}
I always receive the next error at the repository:
error
Is there something I am doing wrong? I have been stuck with this for a week now...
Thanks in advance and sorry for the long text, but I wanted it to be clear.
I could finally solve it by adding .AsNoTracking() at the Get() method of my repository:
public async Task<T> Get(int id, Func<IQueryable<T>, IIncludableQueryable<T, object>> includes)
{
IQueryable <T> query = DbSet.AsNoTracking();
if (includes != null)
{
query = includes(query);
}
return await query.FirstOrDefaultAsync(m => m.Id == id);
}

How to use AutoMapper to map OData enum string in json request dto to entity enum property

I am working on a new ASP.NET Core 3.1.1 API with Microsoft.AspNetCore.OData v 7.3.0, AutoMapper v9.0.0 and Microsoft.AspNetCore.Mvc.NewtonsoftJson v3.1.1
I am getting the following error when I make a POST to the Accounts endpoint using Postman v7.18.0;
AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping.
I have reviewed the similar questions list when creating this question but was unable to find a solution.
In reviewing google searches for AutoMapper OData Enums all I could find were the recommendation to decorate my dto class with...
[AutoMap(typeof(Account))]
... and to decorate my dto enum properties with ...
[JsonConverter(typeof(StringEnumConverter))]
However, I still get the error. I found references to using an AutoMapperProfile class with a mapper defined as
CreateMap<Account, AccountModel>().ReverseMap();
But it appears that AutoMapper v9.0.0 no longer has a CreateMap method. My understanding was that adding the [AutoMap(typeof(Account))] to the dto class had the same effect as creating the map in the profile class.
I feel like I am going in circles at this point here so I though I would reach out to the SO community. I am sure it is something simple, I am just not seeing it.
Here is my POST request body from Postman;
{
"#odata.context": "https://localhost:44367/v1/$metadata#Accounts",
"AccountName": "Test Provider",
"AccountType": "Provider",
"IsTaxExempt": false,
"Status": "Active"
}
Here is my AccountsController Post method;
[ODataRoute]
[Produces("application/json;odata.metadata=minimal")]
[ProducesResponseType(typeof(AccountModel), Status201Created)]
[ProducesResponseType(Status400BadRequest)]
public async Task<IActionResult> Post([FromBody] AccountModel record)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
record.Id = new Guid();
var entity = _mapper.Map<Account>(record);
_context.Add(entity);
await _context.SaveChangesAsync();
var createdRecord = _mapper.Map<AccountModel>(entity);
return Created(createdRecord);
}
Here is my Account entity class;
public class Account : EntityBase
{
[Required]
[Column(TypeName = "nvarchar(50)")]
[MaxLength(50)]
public string AccountName { get; set; }
public AccountTypes AccountType { get; set; }
public bool IsTaxExempt { get; set; }
}
Here is the EntityBase class;
public class EntityBase
{
[Required]
public Guid Id { get; set; }
public DateTimeOffset? DateTimeCreated { get; set; } = DateTime.UtcNow;
public DateTimeOffset? DateTimeLastModified { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public StatusTypes Status { get; set; }
public bool DeleteFlag { get; set; }
}
Here is my Account DTO class;
[Filter, Count, Expand, OrderBy, Page, Select]
[AutoMap(typeof(Account))]
public class AccountModel : BaseModel
{
[Required]
[MaxLength(50)]
public string AccountName { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public AccountTypes AccountType { get; set; }
public bool IsTaxExempt { get; set; }
}
Here is my BaseModel class;
[Select, Filter]
public class BaseModel
{
public Guid Id { get; set; }
public DateTimeOffset DateTimeCreated { get; set; } = DateTime.UtcNow;
public DateTimeOffset DateTimeLastModified { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public StatusTypes Status { get; set; }
public bool DeleteFlag { get; set; }
}
And here are my Enums for AccountTypes and StatusTypes
public enum AccountTypes
{
Customer = 0,
Reseller = 1,
Provider = 2,
}
public enum StatusTypes
{
Active = 0,
Inactive = 1,
}
Any ideas?
It turns out that I needed to create an instance of an AutoMapper MapperConfiguration and assign it to the mapper.
I ended up putting in in the constructor of the Controller, for example;
public AccountsController(CdContext context, IMapper mapper)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
var config = new MapperConfiguration(cfg => cfg.CreateMap<Account, AccountModel>().ReverseMap());
_mapper = new Mapper(config);
}
After I did this, everything worked as expected.
Here is a link to AutoMappers docs on the subject.

Entity Framework 6 - child property data not loading

The ManagingAgent child property on the Complex entity is not being loaded with data.... possibly the result of too much mulled wine.
I have logged the SQL on the database calls and the SQL is returning the correct data.
LazyLoading is disabled.
public ApplicationDbContext()
: base("DefaultConnection")
{
this.Configuration.LazyLoadingEnabled = false;
}
Aggregate Root
public class Complex
{
public Complex()
{
Forums = new List<Forum>();
ManagingAgent = new ManagingAgent();
}
[Key]
public int ComplexId { get; set; }
[Required]
public string Name { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string Address3 { get; set; }
public int? PostCodeId { get; set; }
public PostCode PostCode { get; set; }
public int? LocationId { get; set; }
public Location Location { get; set; }
public int? CountyId { get; set; }
public County County { get; set; }
public int? ManagingAgentId { get; set; }
public ManagingAgent ManagingAgent { get; set; }
public int? CountOfUnits { get; set; }
public List<Forum> Forums { get; set; }
}
Attempt 1. using Include...
public List<Complex> GetComplexesByUserId(Guid userId)
{
using (var db = new ApplicationDbContext())
{
db.Database.Log = Logger;
var complexIds = db.UserApartments.Where(r => r.UserId == userId)
.Select(c => c.ComplexId).ToList();
return db.Complexes.Where(c => complexIds.Contains(c.ComplexId))
.Include(m => m.ManagingAgent).ToList();
}
}
Attempt 2 - explicitly loading ..same result (SQL returns data correctly but ManagingAgent isn't populated)
public List<Complex> GetComplexesByUserId(Guid userId)
{
using (var db = new ApplicationDbContext())
{
db.Database.Log = Logger;
var complexIds = db.UserApartments.Where(r => r.UserId == userId)
.Select(c => c.ComplexId).ToList();
var list = new List<Complex>();
foreach (var id in complexIds)
{
var complex = db.Complexes.Find(id);
db.Entry(complex).Reference(m => m.ManagingAgent).Load();
list.Add(complex);
}
return list;
}
}
So, to force the load I am doing this.... not good..
foreach (var id in complexIds)
{
var complex = db.Complexes.Find(id);
var managingAgent = db.ManagingAgents.Find(complex.ManagingAgentId);
complex.ManagingAgent = managingAgent;
list.Add(complex);
}
Remove this line...
ManagingAgent = new ManagingAgent();
...from the constructor of the Complex entity. Then it will work. (Generally don't instantiate reference navigation properties in an entity default constructor. EF calls this constructor via reflection when it materializes the entity and "gets confused" if the navigation property already has a reference. I can't explain the "gets confused" better since I don't know the exact mechanism of object materialization with related entities, but the effect is that the loaded child column values are ignored because there is already an instantiated child entity, but just with the useless default values from the ManagingAgent constructor.)