Skip to content

Commit

Permalink
fix dynamic controller
Browse files Browse the repository at this point in the history
  • Loading branch information
iwate committed Jul 11, 2023
1 parent e9569fa commit 9eb3553
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 73 deletions.
56 changes: 35 additions & 21 deletions src/ODatalizer.EFCore.Tests/CompositKeyTest.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
using Newtonsoft.Json.Linq;
using ODatalizer.EFCore.Tests.Host;
using ODatalizer.EFCore.Tests.Host;
using Sample.EFCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;

Expand All @@ -21,43 +16,62 @@ public CompositKeyTest(ODatalizerWebApplicationFactory<Startup> factory)
{
_client = factory.CreateClient();
}

[Fact(DisplayName = "GET ~/entitysets(key1,key2)"), TestPriority(0)]
public async Task Find()
/**
* https://docs.oasis-open.org/odata/odata/v4.01/cs01/abnf/odata-abnf-construction-rules.txt
* CompoundKey(multi-part keys) needs to specify key=value.
*/
[Theory(DisplayName = "GET ~/entitysets(name1=key1,name2=key2)"), TestPriority(0)]
[InlineData("/sample/Favorites(UserId=1,ProductId=2)")]
[InlineData("/sample/Favorites(ProductId=2,UserId=1)")]
[InlineData("/sample/Favorites(UserId=1, ProductId=2)")]
// [InlineData("/sample/Favorites/1/2")] // --- not supported yet
public async Task Find(string path)
{
var response = await _client.GetAsync("/sample/Favorites(1,1)");
var response = await _client.GetAsync(path);

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

[Fact(DisplayName = "PUT ~/entitysets(key1,key2)"), TestPriority(1)]
public async Task Put()
[Theory(DisplayName = "PUT ~/entitysets(name1=key1,name2=key2)"), TestPriority(1)]
[InlineData("/sample/Favorites(UserId=1,ProductId=2)")]
[InlineData("/sample/Favorites(ProductId=2,UserId=1)")]
[InlineData("/sample/Favorites(UserId=1, ProductId=2)")]
// [InlineData("/sample/Favorites/1/2")] // --- not supported yet
public async Task Put(string path)
{
var response = await _client.PutAsync("/sample/Favorites(1,1)", Helpers.JSON(new
var response = await _client.PutAsync(path, Helpers.JSON(new
{
UserId = 1,
ProductId = 1,
ProductId = 2,
}));

Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}

[Fact(DisplayName = "PATCH ~/entitysets(key1,key2)"), TestPriority(1)]
public async Task Patch()
[Theory(DisplayName = "PATCH ~/entitysets(name1=key1,name2=key2)"), TestPriority(1)]
[InlineData("/sample/Favorites(UserId=1,ProductId=2)")]
[InlineData("/sample/Favorites(ProductId=2,UserId=1)")]
[InlineData("/sample/Favorites(UserId=1, ProductId=2)")]
// [InlineData("/sample/Favorites/1/2")] // --- not supported yet
public async Task Patch(string path)
{
var response = await _client.PatchAsync("/sample/Favorites(1,1)", Helpers.JSON(new
var response = await _client.PatchAsync(path, Helpers.JSON(new
{
UserId = 1,
ProductId = 1,
ProductId = 2,
}));

Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}

[Fact(DisplayName = "DELETE ~/entitysets(key1,key2)"), TestPriority(2)]
public async Task Delete()
[Theory(DisplayName = "DELETE ~/entitysets(name1=key1,name2=key2)"), TestPriority(2)]
[InlineData("/sample/Favorites(UserId=1,ProductId=1)")]
[InlineData("/sample/Favorites(ProductId=2,UserId=1)")]
[InlineData("/sample/Favorites(UserId=1, ProductId=3)")]
// [InlineData("/sample/Favorites/1/4")] // --- not supported yet
public async Task Delete(string path)
{
var response = await _client.DeleteAsync("/sample/Favorites(1,1)");
var response = await _client.DeleteAsync(path);

Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
Expand Down
12 changes: 12 additions & 0 deletions src/ODatalizer.EFCore/Converters/ITypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace ODatalizer.EFCore.Converters
{
public interface ITypeConverter
{
Type ModelType { get; }
bool CanConvertFrom(Type type);
object Convert(object value);
bool TryParse(string str, out object value);
}
}
60 changes: 60 additions & 0 deletions src/ODatalizer.EFCore/Converters/ODatalizerModelBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ODatalizer.EFCore.Converters
{
public class ODatalizerModelBinder : IModelBinder
{
private readonly IEnumerable<ITypeConverter> _typeConverters;

public ODatalizerModelBinder(IEnumerable<ITypeConverter> typeConverters)
{
_typeConverters = typeConverters;
}

public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;

var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}

bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

var value = valueProviderResult.FirstValue;

if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}

var converter = _typeConverters.FirstOrDefault(o => o.ModelType == bindingContext.ModelType);

if (converter == null)
{
return Task.CompletedTask;
}

if (converter.TryParse(value, out var result))
{
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}

bindingContext.ModelState.TryAddModelError(modelName, "Invalid DateTime format.");

return Task.CompletedTask;
}
}
}
32 changes: 32 additions & 0 deletions src/ODatalizer.EFCore/Converters/ODatalizerModelBinderProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ODatalizer.EFCore.Converters
{
public class ODatalizerModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

var typeConverters = context.Services.GetService<IEnumerable<ITypeConverter>>();

if (typeConverters != null)
{
if (typeConverters.Any(o => context.Metadata.ModelType == o.ModelType))
{
return new BinderTypeModelBinder(typeof(ODatalizerModelBinder));
}
}

return null;
}
}
}
8 changes: 7 additions & 1 deletion src/ODatalizer.EFCore/ODatalizerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Deltas;
using ODatalizer.EFCore.Routing;
using System.Collections.Generic;
using ODatalizer.EFCore.Converters;

namespace ODatalizer.EFCore
{
Expand All @@ -37,7 +39,7 @@ public ODatalizerController(IServiceProvider sp, bool authorize = false)
_logger = sp.GetRequiredService<ILoggerFactory>().CreateLogger<ODatalizerController<TDbContext>>();
_authorization = sp.GetService<IAuthorizationService>();
_authorize = authorize;
_visitor = new ODatalizerVisitor(DbContext);
_visitor = new ODatalizerVisitor(DbContext, sp.GetService<IEnumerable<ITypeConverter>>());
}

[EnableQuery(PageSize = ODatalizerEndpoint.DefaultPageSize)]
Expand All @@ -64,6 +66,10 @@ public virtual async Task<IActionResult> Get()
{
return StatusCode(501);
}
catch (Exception ex) {
_logger.LogError(ex, ex.Message);
throw;
}

if (_visitor.BadRequest)
return BadRequest(ModelState);
Expand Down
34 changes: 31 additions & 3 deletions src/ODatalizer.EFCore/ODatalizerVisitor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.EntityFrameworkCore;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
using Newtonsoft.Json.Linq;
using ODatalizer.EFCore.Converters;
using System;
using System.Collections;
using System.Collections.Generic;
Expand All @@ -20,11 +24,13 @@ public class ODatalizerVisitor
private readonly static Type[] _emptyTypes = Array.Empty<Type>();
private readonly DbContext _db;
private readonly Type _dbType;
private readonly IEnumerable<ITypeConverter> _typeConverters;

public ODatalizerVisitor(DbContext db)
public ODatalizerVisitor(DbContext db, IEnumerable<ITypeConverter> typeConverters)
{
_db = db;
_dbType = db.GetType();
_typeConverters = typeConverters ?? Enumerable.Empty<ITypeConverter>();
}
public bool NotFound { get; protected set; }
public bool BadRequest { get; protected set; }
Expand Down Expand Up @@ -220,17 +226,39 @@ public Task VisitAsync(PathTemplateSegment segment)
throw new NotSupportedException();
}

private static readonly Type _dateTimeType = typeof(DateTime);
private static readonly Type _dateTimeOffsetType = typeof(DateTimeOffset);
private static readonly MethodInfo _compareDateTimeType = _dateTimeType.GetMethod(nameof(DateTime.Compare));
private static readonly MethodInfo _compareDateTimeOffsetType = _dateTimeOffsetType.GetMethod(nameof(DateTimeOffset.Compare));
public object Find(IQueryable queryable, IEnumerable<KeyValuePair<string, object>> keys)
{
var type = queryable.ElementType;
var param = Expression.Parameter(type);
var body = keys.Select(key => Expression.Equal(Expression.Property(param, type.GetProperty(key.Key)), Expression.Constant(key.Value))).Aggregate((l, r) => Expression.And(l, r));
var body = keys.Select(key => {
var prop = type.GetProperty(key.Key);
var value = As(key.Value, prop.PropertyType);
return Expression.Equal(Expression.Property(param, prop), Expression.Constant(value));
}).Aggregate((l, r) => Expression.And(l, r));
var lambda = Expression.Lambda(body, param);
var filter = _where.MakeGenericMethod(type).Invoke(null, new object[] { queryable, lambda });
var first = _firstOrDefault.MakeGenericMethod(type).Invoke(null, new[] { filter });

return first;
}
public object As(object src, Type dstType)
{
var srcType = src.GetType();
if (dstType == srcType)
return src;

var converter = _typeConverters.FirstOrDefault(o => o.ModelType == dstType && o.CanConvertFrom(srcType));
if (converter != null)
{
return converter.Convert(src);
}

throw new InvalidCastException();
}
}

public class ODatalizerAuthorizationInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,12 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
candidates.SetValidity(i, false);
}
}
catch (ODataUnrecognizedPathException ex)
catch (ODataUnrecognizedPathException ex)

Check warning on line 90 in src/ODatalizer.EFCore/Routing/ODatalizerRoutingMatcherPolicy.cs

View workflow job for this annotation

GitHub Actions / windows

The variable 'ex' is declared but never used

Check warning on line 90 in src/ODatalizer.EFCore/Routing/ODatalizerRoutingMatcherPolicy.cs

View workflow job for this annotation

GitHub Actions / linux

The variable 'ex' is declared but never used
{
odataFeature.RoutePrefix = metadata.Prefix;
odataFeature.Model = model;
odataFeature.Path = new ODataPath(new UnrecognizedPathSegment());
}
catch { }
}
return Task.CompletedTask;
}
Expand Down
12 changes: 11 additions & 1 deletion src/ODatalizer.EFCore/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.OData;
using Microsoft.AspNetCore.OData.Batch;
using Microsoft.AspNetCore.OData.NewtonsoftJson;
Expand All @@ -7,8 +8,10 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using ODatalizer.Batch;
using ODatalizer.EFCore.Builders;
using ODatalizer.EFCore.Converters;
using ODatalizer.EFCore.Routing;
using System;
using System.Collections.Generic;

namespace ODatalizer.EFCore
{
Expand Down Expand Up @@ -49,4 +52,11 @@ public static void AddODatalizer(this IServiceCollection services, Func<IService
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, ODatalizerRoutingMatcherPolicy>());
}
}
public static class MvcOptionsExtensions
{
public static void AddODatalizerOptions(this MvcOptions options)
{
options.ModelBinderProviders.Insert(0, new ODatalizerModelBinderProvider());
}
}
}
Loading

0 comments on commit 9eb3553

Please sign in to comment.