umbraco / Umbraco.UIBuilder.Issues

Back office UI builder for Umbraco
3 stars 2 forks source link

Using GUID as a Foreign Key in context app always returns default value #108

Open philipdanielhayton opened 2 months ago

philipdanielhayton commented 2 months ago

Describe the bug We have a custom Reviews table that uses the Node Key (Guid) to map a review to a given umbraco node.

We also have a Context App created through the Umbraco UI builder to manage reviews on the node.

We were using a custom TypeConverter to map the UDI provided by UI Builder to a GUID and vice versa.

However after upgrading from 13.1.1 to version 13.1.5 and the conversion has stopped happening, so the Guid value is always default (0000-000000.... etc). The TypeConverter doesn't event seem to be getting called any more.

Do we need to be using a different approach when using a Guid as the foreign key now?

Steps To Reproduce Steps to reproduce the behavior:

  1. Create a custom database table that includes a umbraco node content key (Guid)
  2. Create a repository to act as intermediary between the db table and UI builder
  3. Create a context app that uses the repository created in (2), and use the umbraco node content key as the foreign key
  4. Add a few entries into the database table for a specific node, just so you know there should be some results
  5. Navigate to the node in Umbraco, flick to the context app, the results should be empty
  6. If you put a breakpoint on the Repository.GetAllImpl method and inspect the whereClause, it should be passing a default Guid (0000-000...)

Expected behavior Ideally, when using a Guid as a foreign key, Umbraco UI would automatically convert the UDI to a Guid, passing the correct Guid into the GetAllImpl whereClause.

However failing that, the old behavior should still work as it did in version 13.1.1. Whereby a custom TypeConverter can be implemented to handle the mapping.

Environment (please complete the following information):

Additional context Here's the UdiToGuidConverter implementation mentioned above.

public class UdiToGuidConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
    {
        return sourceType == typeof(Udi) || sourceType == typeof(GuidUdi) || sourceType == typeof(string);
    }

    public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        return Convert(value);
    }

    public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
    {
        return destinationType == typeof(Guid);
    }

    public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
    {
        return Convert(value);
    }

    private object Convert(object? value)
    {
        if (value is Udi udiValue)
            return ConvertUdiToGuid(udiValue);

        if (value is GuidUdi guidValue)
            return guidValue.Guid;

        if (value is string strValue)
            return ConvertStringToGuid(strValue);

        throw new NotSupportedException($"Cannot convert {value?.GetType()} to Guid.");
    }

    private Guid ConvertUdiToGuid(Udi udi)
    {
        return ((GuidUdi)udi).Guid;
    }

    private Guid ConvertStringToGuid(string strValue)
    {
        if (Guid.TryParse(strValue, out var guid))
            return guid;

        if (UdiParser.TryParse(strValue, out var _udi))
            return ((GuidUdi)_udi).Guid;

        throw new ArgumentException("Provided string cannot be parsed into Guid."); // Or another kind of handling
    }
}

We register this in a composer on start up.

    public void Compose(IUmbracoBuilder builder)
    {
        builder.Services.AddTransient<IReviewService, ReviewService>();
    }

Here's our custom repository:


internal class ReviewUiBuilderRepository : Repository<Review, ReviewId>
{
    private readonly IReviewRepository _reviewRepository;

    public ReviewUiBuilderRepository(RepositoryContext context, IReviewRepository reviewRepository) : base(context)
    {
        _reviewRepository = reviewRepository;
    }

    protected override ReviewId GetIdImpl(Review entity)
    {
        return entity.Id;
    }

    protected override Review GetImpl(ReviewId id)
    {
        return _reviewRepository.FindAsync(id).Result;
    }

    protected override IEnumerable<TJunctionEntity> GetRelationsByParentIdImpl<TJunctionEntity>(ReviewId parentId, string relationAlias)
    {
        return Enumerable.Empty<TJunctionEntity>();
    }

    protected override Review SaveImpl(Review entity)
    {
        return entity;
    }

    protected override TJunctionEntity SaveRelationImpl<TJunctionEntity>(TJunctionEntity entity)
    {
        throw new NotImplementedException();
    }

    protected override void DeleteImpl(ReviewId id)
    {
        throw new NotImplementedException();
    }

    protected override IEnumerable<Review> GetAllImpl(Expression<Func<Review, bool>>? whereClause = null, Expression<Func<Review, object>>? orderBy = null,
        SortDirection orderByDirection = SortDirection.Ascending)
    {
        var where = whereClause ?? (review => true); 
        return _reviewRepository.QueryAsync(where, orderBy, orderByDirection.ToSortOrder()).Result;
    }

    protected override PagedResult<Review> GetPagedImpl(int pageNumber, int pageSize, Expression<Func<Review, bool>>? whereClause = null, Expression<Func<Review, object>>? orderBy = null,
        SortDirection orderByDirection = SortDirection.Ascending)
    {
        var allItems = GetAllImpl(whereClause, orderBy, orderByDirection).ToList();
        var results = new PagedResult<Review>(allItems.Count, pageNumber, pageSize)
        {
            Items = allItems.Skip(pageSize * (pageNumber - 1)).Take(pageSize)
        };

        return results;
    }

    protected override long GetCountImpl(Expression<Func<Review, bool>> whereClause)
    {
        return GetAllImpl(whereClause).Count();
    }
}