JonPSmith / EfCore.GenericServices

A library to help you quickly code CRUD accesses for a web/mobile/desktop application using EF Core.
https://www.thereformedprogrammer.net/genericservices-a-library-to-provide-crud-front-end-services-from-a-ef-core-database/
MIT License
601 stars 94 forks source link

NullReferenceException using ILinkToEntity and embedded resources for CosmosDb #43

Closed rfcdejong closed 4 years ago

rfcdejong commented 4 years ago

Using CosmosDb with Entity Framework Core it is not possible to use nested resources. Having the following code:

    public class Person : EntityEvents
    {
        public string TenantId { get; private set; }
        public Guid PersonId { get; private set; }
        public string? CallName { get; private set; }
        public string? FirstName { get; private set; }
        public string? Initials { get; private set; }
        public string? Prefix { get; private set; }
        public string LastName { get; private set; }

        public PersonAddress Address { get; private set; }

#nullable disable
        public Person() { }
#nullable restore

        public Person(Guid tenantId, Guid personId, string? callName, string? firstName, string? initials, string? prefix, string lastName,
            string? street = null, string? houseNumber = null, string? houseNumberAddition = null, string? city = null, string? postalCode = null, string? country = null)
        {
            TenantId = tenantId.ToString();
            PersonId = personId;
            CallName = callName;
            FirstName = firstName;
            Initials = initials;
            Prefix = prefix;
            LastName = lastName;

            Address = new PersonAddress(street, houseNumber, houseNumberAddition, city, postalCode, country);
        }
    }

    public class PersonAddress
    {
        public string? Street { get; private set; }
        public string? HouseNumber { get; private set; }
        public string? HouseNumberAddition { get; private set; }
        public string? City { get; private set; }
        public string? PostalCode { get; private set; }
        public string? Country { get; private set; }

#nullable disable  
        public PersonAddress() { }
#nullable  restore

        public PersonAddress(string? street, string? houseNumber, string? houseNumberAddition, string? city, string? postalCode, string? country)
        {
            Street = street;
            HouseNumber = houseNumber;
            HouseNumberAddition = houseNumberAddition;
            City = city;
            PostalCode = postalCode;
            Country = country;
        }
    }

    public class PersonTypeConfiguration : IEntityTypeConfiguration<Person>
    {
        public void Configure(EntityTypeBuilder<Person> builder)
        {
            builder.HasPartitionKey(x => x.TenantId);
            builder.HasKey(x => x.PersonId);
            builder.OwnsOne(x => x.Address);
        }
    }

With some Dto's

    public class PersonDto : ILinkToEntity<Domain.AggregatesModel.PersonAggregate.Person>
    {
        [ReadOnly(true)]
        public Guid PersonId { get; set; }
        public string? CallName { get; set; }
        public string? FirstName { get; set; }
        public string? Initials { get; set; }
        public string? Prefix { get; set; }
        public string LastName { get; set; }

        public PersonAddressDto Address { get; set; }
    }

    public class PersonAddressDto : ILinkToEntity<PersonAddress>
    {
        public string? Street { get; set; }
        public string? HouseNumber { get; set; }
        public string? HouseNumberAddition { get; set; }
        public string? City { get; set; }
        public string? PostalCode { get; set; }
        public string? Country { get; set; }
    }

    public class PersonDtoConfig : PerDtoConfig<PersonDto, Domain.AggregatesModel.PersonAggregate.Person>
    {
        public override Action<IMappingExpression<Domain.AggregatesModel.PersonAggregate.Person, PersonDto>> AlterReadMapping =>
            cfg => cfg
                .ForMember(x => x.Address, x =>
                    x.MapFrom(person => person.Address));
    }

EF does not allow to define the entity without a key and using OwnsOne, but I can leave the follow line away. modelBuilder.Entity<PersonAddress>(pa => pa.HasNoKey());

Setup and then Registering the DtoType's crashes because it expects a primaryKeyProperty on the entity, the entityInfo.PrimaryKeyProperties in DecodedDto contains a property with the value null and then it crashes.

image

rfcdejong commented 4 years ago

Sadly, just adding a Key on an new Id property to PersonAddress doesn't do the trick. Then at runtime it turns into another null reference exception

   at lambda_method(Closure , QueryContext , JObject )
   at Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable`1.Enumerator.MoveNext()
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source)
   at lambda_method(Closure , QueryContext )
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at System.Linq.Queryable.FirstOrDefault[TSource](IQueryable`1 source)
   at MyProject.API.Controllers.PersonController.GetSingleAsync(Guid personId, ICrudServices`1 service) in C:\Repos\MyProject\Controllers\PersonController.cs:line 50
   at lambda_method(Closure , Object )

For now I will not use ILinkToEntity and will try to use Automapper directly

JonPSmith commented 4 years ago

If you look up about Owned types you will see the class is contained in the outer entity class, i.e. the data becomes part of the outer entity class. Therefore you can only access that data via the outer class.

This means that the adding ILinkToEntity<PersonAddress> isn't going to work. You should be using nested DTOs as described in the GenericServices's Wiki.