Динамическая маршрутизация в BaseUrl в Asp.Net Core OData 4.0
В настоящее время я разрабатываю OData Api для приложения C# Asp.Net Core.
Чтобы соответствовать спецификациям нашего API, URL должен соответствовать нашей мультитенантной архитектуре:https://website.com/api/tenants/{tenantId}/odata/
Поскольку в OData 4.0 нет спецификации для реализации динамического базового URL-адреса, я реализовал следующий обходной путь: используйте промежуточное программное обеспечение для замены динамического tenantId в контексте HTTP статической строкой "tenantId". Теперь мне нужно найти способ изменить / манипулировать метаданными OData, чтобы отменить этот обходной путь в ответе.
Пример реализации
Starup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
private IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDependencies(Configuration);
services.AddDbContext<DBContext>();
services.AddOData();
services.AddODataQueryFilter();
services.AddAutoMapper();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Custom Workaround Middleware
app.Use(async (context, next) =>
{
// TGis Method parses the tenant id from the Request.Path, replaces it and wries it to the context.Items to maintain the information for later
(Microsoft.AspNetCore.Http.HttpContext contextwTid, System.Guid tenantGuid) = ODataHelper.ParseTenantIDToContext(context);
context = contextwTid;
await next.Invoke();
});
app.UseMvc(b =>
{
b.Select().Filter().OrderBy().MaxTop(100).Count();
b.MapODataServiceRoute(
routeName: "odata",
routePrefix: "api/tenants/tenantId/odata",
model: ODataHelper.GetEdmModel());
});
}
ODataHelper:
...
public static (Microsoft.AspNetCore.Http.HttpContext, Guid) ParseTenantIDToContext(Microsoft.AspNetCore.Http.HttpContext context)
{
System.Guid tenantGuid = System.Guid.Empty;
if (context.Request.Path.ToString().Split('/').Length > 3 && context.Request.Path.ToString().ToLower().Contains("odata"))
{
bool isValidGUID = System.Guid.TryParse(context.Request.Path.ToString().Split('/')[3], result: out tenantGuid);
if (isValidGUID)
context.Request.Path = context.Request.Path.Value.Replace(context.Request.Path.ToString().Split('/')[3], "tenantId");
context.Items["tenantId"] = tenantGuid.ToString();
}
return (context, tenantGuid);
}
...
Пример контроллера:
public class ClientsController : ODataController
{
private readonly DBService<Client> _service;
public ClientsController(DBService<Client> service)
{
_service = service;
}
[HttpGet]
[EnableQuery]
[ODataRoute("Clients")]
public async Task<IEnumerable<Client>> Get(
ODataQueryOptions<Client> options)
{
System.Guid tenantId = ODataHelper.GetTenantIDFromContext(this.HttpContext);
IQueryable res = await _service.Get(
tenantId,
AuthorizationHelper.GetSubjectId(tenantId, User),
AuthorizationHelper.GetAllowedUserRoles(RoleType.Reporting),
options,
null);
return new List<Client>(res.Cast<Client>());
}
}
Вопросы:
- Есть ли лучший способ реализовать динамическую базовую маршрутизацию в OData с Asp.Net Core?
- Есть ли способ манипулировать запросом или метаданными OData. Подробно ответ должен отображать исходный URL с динамическим tenantId в "@OData.context" и (на будущее) в метаданных подкачки OData.
Исследования / поиск в Google до сих пор:
- ODataMediaTypeFormatter в WebApi, но я не нашел реализации для.net Core.
- Решение для WebApi 2.2, но в.net Core нет UrlHelper.
- Официальный учебник по OData WebApi, но это не является более простым способом, чем трудоемкость.
1 ответ
РЕДАКТИРОВАТЬ 2: Иногда вы думаете, что так сложно, что вы упускаете очевидное. Решение для динамической маршрутизации с OData:
Startup.cs
app.UseMvc(b =>
{
b.Select().Filter().OrderBy().MaxTop(100).Count();
b.MapODataServiceRoute(
routeName: "odata",
routePrefix: "api/tenants/{tenantId}/odata",
model: ODataHelper.GetEdmModel());
});
контроллер:
[HttpGet]
[EnableQuery]
[ODataRoute("Clients")]
public async Task<IEnumerable<Client>> Get(
ODataQueryOptions<Client> options,
[FromRoute] Guid tenantId)
{
IQueryable res = await _service.Get(
tenantId,
AuthorizationHelper.GetSubjectId(tenantId, User),
AuthorizationHelper.GetAllowedUserRoles(RoleType.Reporting),
options,
null);
return new List<Client>(res.Cast<Client>());
}
Я оставляю здесь свой обходной путь для случая, когда кто-то может использовать его:
После значительного исследования в реализации OData .Net Core я, наконец, обнаружил, что моя первая предоставленная ссылка " ODataMediaTypeFormatter в WebApi" уже обеспечила решение моего обходного пути.
Во-первых, BaseAddressFactory
может только данный HTTP-запрос. Поэтому мне нужно было изменить следующий код:
public static (Microsoft.AspNetCore.Http.HttpContext, Guid) ParseTenantIDToContext(Microsoft.AspNetCore.Http.HttpContext context)
{
System.Guid tenantGuid = System.Guid.Empty;
if (context.Request.Path.ToString().Split('/').Length > 3 && context.Request.Path.ToString().ToLower().Contains("odata"))
{
bool isValidGUID = System.Guid.TryParse(context.Request.Path.ToString().Split('/')[3], result: out tenantGuid);
if (isValidGUID)
{
context.Request.Path = context.Request.Path.Value.Replace(context.Request.Path.ToString().Split('/')[3], "tenantId");
context.Items["tenantId"] = tenantGuid.ToString();
context.Request.Headers.Remove("tenantId");
context.Request.Headers.Append("tenantId", tenantGuid.ToString());
}
}
return (context, tenantGuid);
}
В этом разделе я сохраняю необходимые tenantId
не только в HTTPContext, но и в качестве специального заголовка в HTTPRequest.
Основным решением является предоставление специального BaseAddressFactory
функция, которая манипулирует базовым адресом, который OData использует для создания метаданных. В качестве реализации я добавляю следующий код в ConfigureServices
после добавления OData через services.AddOData()
:
services.AddMvc(op =>
{
foreach (var formatter in op.OutputFormatters
.OfType<ODataOutputFormatter>())
{
formatter.BaseAddressFactory = ODataHelper.CustomBaseAddressFactory;
}
foreach (var formatter in op.InputFormatters
.OfType<ODataInputFormatter>())
{
formatter.BaseAddressFactory = ODataHelper.CustomBaseAddressFactory;
}
});
мой ODataHelper.CustomBaseAddressFactory
выглядит так:
public static Uri CustomBaseAddressFactory (HttpRequest request)
{
Guid tenantGuid = GetTenantIDFromRequest(request);
request.Headers.Remove("tenantId");
Uri std = ODataInputFormatter.GetDefaultBaseAddress(request);
string ret = replaceTentantIdInURL(std.ToString(), tenantGuid);
return ret[ret.Length - 1] != '/' ? new Uri(ret + '/') : new Uri(ret);
}
Для обеспечения максимально возможной совместимости я использую стандарт ODataInputFormatter.GetDefaultBaseAddress
и впоследствии заменяет мой статический заполнитель снова.
РЕДАКТИРОВАТЬ
Этот способ сохранения tenantId
довольно небезопасно, поскольку заголовки запроса могут быть созданы также и конечным пользователем. В конце концов, я решил получить ID из наших требований авторизации, которые его предоставляют. Поэтому пользователь не может атаковать этот обходной путь.