本文屬於OData系列
目錄
- 武裝你的WEBAPI-OData入門
- 武裝你的WEBAPI-OData便捷查詢
- 武裝你的WEBAPI-OData分頁查詢
- 武裝你的WEBAPI-OData資源更新Delta
- 武裝你的WEBAPI-OData之EDM
- 武裝你的WEBAPI-OData使用Endpoint
- 武裝你的WEBAPI-OData常見問題
非常喜歡OData,在各種新專案中都使用了這個技術。對於.NET 5.0,OData
推出了8.0preview,於是就試用了一下。發現坑還是非常多,如果不是很有必要的話,建議還是先等等。我使用的原因是在.NET 5.0的情況,7.x版本的OData會造成[Authorize]
無法正常工作,導致許可權認證無法正常進行。
環境
執行環境如下:
- ASP.NET CORE WEBAPI ON .NET 5.0
- Microsoft.AspNetCore.Authentication.JwtBearer 5.0.2
- Swashbuckle.AspNetCore 5.6.3
- Microsoft.AspNetCore.OData 8.0.0-preview3
由於Microsoft.AspNetCore.OData.Versioning.ApiExplorer
這個庫不支援新版的OData
,所以版本控制只能使用OData 8.0.0自帶的路由方式控制。
常見問題
提示Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Conflicting method/path combination "GET api/WeatherForecast" for actions - Actions require a unique method/path combination for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a workaround
路由的形式有了變化,OData 8.0.0中,在Controller上標記了ODataRoutePrefix之後,不要標記無引數的ODataRoute。現在ODataRoute會從ODataRoutePrefix開始路由,如果標記無引數的ODataRoute,實際上相當於標記了兩次,則系統會認為有兩個相同的方法,操作重複路由。對於一個有引數,一個無引數的,可以給有引數的方法標記[ODataRoute("id")]。有一個例外,如果引數名稱是key
,那麼可以不標記。
注意,請不要直接使用[HttpGet("id")]的形式給OData指定路由,這個形式會直接忽略掉OData直接從/
開始路由。
其實我也覺得新的方式才是更合理的。
提示 Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Ambiguous HTTP method for action - Microsoft.AspNetCore.OData.Routing.Controllers.MetadataController.GetMetadata (Microsoft.AspNetCore.OData). Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0
我推測應該是bug,在Controller方法只有一個Get並且明確標誌了[HttpGet]的形式,依然提示錯誤。這個問題可以參考這裡。
services.AddSwaggerGen(options =>
{
// ........................
options.DocInclusionPredicate((name, api) => api.HttpMethod != null);
});
提示System.InvalidOperationException: No media types found in 'Microsoft.AspNetCore.OData.Formatter.ODataOutputFormatter.SupportedMediaTypes'. Add at least one media type to the list of supported media types.
這個我估摸也是bug,請注意,必須將services.AddOData放在services.AddControllers之前,否則在Controller中無法識別ODataOutputFormatter,然後參考這裡解決問題。
services.AddControllers(
options =>
{
foreach (var outputFormatter in options.OutputFormatters.OfType<ODataOutputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
}
foreach (var inputFormatter in options.InputFormatters.OfType<ODataInputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
}
});
提示ODM的問題The entity set 'WeatherForecast' is based on type 'WebApplication2.WeatherForecast' that has no keys defined.
現在EdmBuilder不能識[Key]來進行主鍵的標記了,需要顯式新增HasKey
:
var configuration = builder.EntitySet<WeatherForecast>("WeatherForecast").EntityType.HasKey(w=>w.TemperatureC);
詭異的key與id問題
如果資料模型使用的主鍵,在函式中籤名為key
,大部分操作都很正常;如果使用id
就會出現各種形形色色的問題,比如不能正確識別函式過載、無法載入路由等問題。感覺和那個Conventional Routing有關係,實在是折騰不動了,老實使用key
算了。
詭異的返回所有資料只有主鍵id的問題
返回資料數量是正確的,但是隻能返回主鍵id,其他屬性通通沒有。這是因為原來使用ODataModelBuilder
已經不能正確工作了,現在需要更換成ODataConventionModelBuilder
才可以正常工作對映。
返回首字元大小寫的問題
之前版本返回的都是預設的小寫字母開頭的CamelCase,這個版本預設直接返回PascalCase,對前端不是很友好,需要設定一下轉換才可以。
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EnableLowerCamelCase();
實體類繼承abstract導致的找不到基類的屬性問題
實體類在abstract基類中的屬性,還是本質上還是屬於基類,預設情況不在EDM中註冊也是可以訪問的,但是如果設定非預設的行為(比如設定了大小寫),那會出現無法訪問基類屬性的現象(基類行為和實體類行為不一致),這個時候需要在EDM中對基類進行註冊(即使沒有對應的Controller或者其他引用),參考這個回答。
完整程式碼
最後貼一下可以正常執行的程式碼:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.OData;
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Net.Http.Headers;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
using Microsoft.OpenApi.Models;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Linq;
using System.Text;
namespace WebApplication2
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtRegisteredClaimNames.Sub,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
//options.Authority = "https://222.31.160.20:5001";
});
services.AddCors(options =>
{
options.AddDefaultPolicy(
builder =>
{
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
});
});
services.AddOData(opt => opt.AddModel("api", GetEdmModel()).Expand().Filter().Count().OrderBy().Filter());
services.AddControllers(
options =>
{
foreach (var outputFormatter in options.OutputFormatters.OfType<ODataOutputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
}
foreach (var inputFormatter in options.InputFormatters.OfType<ODataInputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
}
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication2", Version = "v1" });
var filePath = Path.Combine(System.AppContext.BaseDirectory, "WebApplication2.xml");
c.IncludeXmlComments(filePath);
c.DocInclusionPredicate((name, api) => api.HttpMethod != null);
});
}
private IEdmModel GetEdmModel()
{
ODataModelBuilder builder = new ODataModelBuilder();
var configuration = builder.EntitySet<WeatherForecast>("WeatherForecast").EntityType.HasKey(w=>w.TemperatureC);
return builder.GetEdmModel();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (true)
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication2 v1"));
}
app.UseCors();
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseStaticFiles();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}