zoukankan      html  css  js  c++  java
  • 武装你的WEBAPI-OData常见问题

    本文属于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();
                });
            }
    
    
        }
    }
    
    除非特殊说明,本作品由podolski创作,采用知识共享署名 4.0 国际许可协议进行许可。欢迎转载,转载请保留原文链接~喜欢的观众老爷们可以点下关注或者推荐~
  • 相关阅读:
    浏览器窗口的尺寸和大小
    Oracle
    Maven
    框架使用xm配置文件中文件头信息
    Oracle SQL Developer 安装
    Jquery函数的几种写法
    spring boot拦截器配置
    java之大文件断点续传
    idea打jar包经验总结
    oracle模糊搜索避免使用like,替换为instr()
  • 原文地址:https://www.cnblogs.com/podolski/p/14303517.html
Copyright © 2011-2022 走看看