在上一篇文章中,老周给大伙伴们大致说了下 MVC 下的模型绑定,今天咱们进行一下细化,先聊聊模型绑定中涉及到的一些组件对象。

------------------------------------------------------------------------------

一、ValueProvider——提取绑定源的值

首先登场的小帅哥是 ValueProvider,即实现 IValueProvider 接口。

public interface IValueProvider
{
bool ContainsPrefix(string prefix);
ValueProviderResult GetValue(string key);
}

提取绑定源的值在操作上类似字典对象的访问,通过一个指定的 key 来检索。这个主要针对数据结构类似字典的数据源,比如

1、HTTP Header,它的结构就是 name: value;

2、Form 对象,比如 HTML 页上的<form>元素,或者客户端直接提交的 form-data,当然包括用 JQuery 等方式提交的 form;

3、Route Value,也就是路由参数。比如咱们在写MVC时很熟悉的那个 {controller}/{action},若访问的是 Home/Index,那么这里面就是两个数据项。第一个 key 是 controller,value 是 Home;第二个 key 是 action,value 是 Index。

于是,.net core 中很自然地会内置一些已实现的 provider。

FormValueProvider
FormFileValueProvider
JQueryFormValueProvider
RouteValueProvider
HeaderValueProvider

看着它们的大名,估计你也能猜到它们的作用。你会问:咦,HeaderValueProvider 在哪,我咋没看到?这厮藏得比大 Boss 还深!它是在 HeaderModelBinder 文件中定义的私有类,所以我们根本访问不到它。

    private class HeaderValueProvider : IValueProvider

许多 ValueProvider 类型还有专配的 ValueProviderFactory,实现 IValueProviderFactory 接口。该接口只需实现一个方法:CreateValueProviderAsync。

public Task CreateValueProviderAsync(ValueProviderFactoryContext context)

返回值只是个 Task?那创建的 ValueProvider 实例放到哪?注意它有个参数是个上下文对象(ValueProviderFactoryContext),此对象有个属性叫 ValueProviders。创建的 ValueProvider 实例被添加到这个列表中。

也就是说,可以调用各个 Factory 对象创建多种 ValueProvider,然后都添加进 ValueProviders  列表中。当我们自己编写 ModelBinder 时,就可以从这个列表中的 N 个 ValueProvider 对象中提取值。这个 ValueProvider 的值会自动被合并,我们不需要关心它来自哪个 ValueProvider 对象。

比如,列表中有 QueryString 和 RouteValue 两个值来源,其中 key1 来自 QueryString,key2 来自 RouteValue,我在自定义 binder 时,不必管它从哪里来,我只要知道有两个键叫 key1、key2 就行。

    public Task BindModelAsync(ModelBindingContext bindingContext)
{
…… var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
……
return Task.CompletedTask;
} var modelState = bindingContext.ModelState;
modelState.SetModelValue(modelName, valueProviderResult); var metadata = bindingContext.ModelMetadata;
var type = metadata.UnderlyingOrModelType;
try
{
var value = valueProviderResult.FirstValue; object? model;
if (string.IsNullOrWhiteSpace(value))
{
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
model = null;
}
else if (type == typeof(float))
{
model = float.Parse(value, _supportedStyles, valueProviderResult.Culture);
}
else
{
// unreachable
throw new NotSupportedException();
} // When converting value, a null model may indicate a failed conversion for an otherwise required
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
// current bindingContext. If not, an error is logged.
if (model == null && !metadata.IsReferenceOrNullableType)
{
modelState.TryAddModelError(
modelName,
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
valueProviderResult.ToString()));
}
else
{
bindingContext.Result = ModelBindingResult.Success(model);
}
}
catch (Exception exception)
{
…… } _logger.DoneAttemptingToBindModel(bindingContext);
return Task.CompletedTask;
}

以上是从源代码中抄来的一段,用于绑定 float 类型的 binder。

内部已经提供基础类型和复合类型的 Binder,所以一般情况下我们不需要自己花时间去写 Binder。

二、Binder 对象

binder 对象必须实现 IModelBinder 接口。这个接口只要求实现一个方法。

    Task BindModelAsync(ModelBindingContext bindingContext);

在这个方法的实现在,你要完成从数据源中提取值,然后产生绑定目标对象的过程。

例如,你的控制器类中有这么个方法(API方法):

public Task UpdateStudent(Student stu)
{
……
}

假设这个 Student 类有 Name、Age、Email 三个属性,不管数据是通过 URL 查询字符串传递还是 form 提交,都需要提供这些值:

name=小明

age=21

email=abc@163.com

于是,你的自定义 Binder 可以这样写

public class StudentBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
// 提取值
var valname = bindingContext.ValueProvider.GetValue("name");
var valemail = bindingContext.ValueProvider.GetValue("email");
var valage = bindingContext.ValueProvider.GetValue("age");
// 剥出真实的值
string? name;
if(valname != ValueProviderResult.None)
{
name = valname.FirstValue;
}
int age;
if(valage != ValueProviderResult.None)
{
_ = int.TryParse(valage.FirstValue, out age);
}
string? email;
if(valemail != ValueProviderResult.None)
{
email = valemail.FirstValue;
}
// 实例化目标对象
Student s = new()
{
Name = name,
Age = age,
Email = email
};
// 如果绑定成功,必须设置 Result
bindingContext.Result = ModelBindingResult.Success(s);
}
}

绑定的过程可以很简单,也可以弄得很复杂,主要看你的需求。bindingContext 对象的 Result 属性一定要设置,默认是绑定失败。传递给 ModelBindingResult.Success(s) 方法的参数就是目标对象。比如上面的,通过模型绑定的目标是给 Student 对象的属性赋值,所以传递的就是 Student 实例的引用。控制器方法 UpdateStudent 的参数就会引用这个 Student 实例,绑定完成。

其实上面的 Binder 我只是写着玩的,而且它只局限在 Student 类上,不能通用于所有类型。实际上咱们也不需写,我只是做演示。内置的 ComplexObjectModelBinder 类就能完成这项工作,而且它是通用于所有复合类型。

binder 写好了怎么用呢?这分为全局局部。像上面例子这种特定于 Student 类型的 binder,最好还是局部应用——在 Student 类上通过特性类来关联。

[ModelBinder(typeof(StudentBinder))]
public class Student
{
……
}

不想写在类,也可以写在控制器方法的参数上。

public Task UpdateStudent([ModelBinder(typeof(StudentBinder))] Student stu)

三、ModelBinderProvider

如果你希望自定义的 binder 可以应用于全局,那你得实现 IModelBinderProvider 接口。这个接口只有一个方法:

IModelBinder? GetBinder(ModelBinderProviderContext context);

通过这个方法你会发现,ModelBinderProvider 的作用就是获取 binder 对象。

所以咱们上面那个例子,可以写一个 StudentBinderProvider 类,实现 GetBinder 方法,返回一个 binder 实例。

    return new StudentBinder();

既然是全局的,当然要在应用程序初始化时完成。在 Program.cs 文件中,通过 MvcOptions 对象来配置。

var builder = WebApplication.CreateBuilder();
builder.Services.AddControllers(opt =>
{
opt.ModelBinderProviders.Insert(0, new StudentBinderProvider());
});
var app = builder.Build(); app.MapControllers(); app.Run();

不过,上面写的那个例子,应用到全局后容易翻车。因为所有 MVC 请求都会调用,而上面代码中咱们是把 StudentBinderProvider 对象放在列表的首位,只要有 MVC 请求,都会调用它来获取 binder,结果所有类型的绑定目标都会用 StudentBinder 来做绑定,不是 Student 类型的对象就无法绑定,获取不到数据源的值。

当然你可以做一下类型判断,不是 Student 的值接返回 null。这样运行时会转而尝试其他 ModelBinderProvider。

    public IModelBinder? GetBinder(ModelBinderProviderContext context)
{ if(context.Metadata.ModelType == typeof(Student))
{
return new .....
} return null;
}

ModelBinderProvider 不需要放到依赖注入容器中,在配置 MVC 功能时会默认添加,自定义的可以用上面的方法通过 MvcOptions 添加。

        // Set up ModelBinding
options.ModelBinderProviders.Add(new BinderTypeModelBinderProvider());
options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory, options));
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
options.ModelBinderProviders.Add(new FloatingPointTypeModelBinderProvider());
options.ModelBinderProviders.Add(new EnumTypeModelBinderProvider(options));
options.ModelBinderProviders.Add(new DateTimeModelBinderProvider());
options.ModelBinderProviders.Add(new TryParseModelBinderProvider());
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
options.ModelBinderProviders.Add(new FormFileModelBinderProvider());
options.ModelBinderProviders.Add(new FormCollectionModelBinderProvider());
options.ModelBinderProviders.Add(new KeyValuePairModelBinderProvider());
options.ModelBinderProviders.Add(new DictionaryModelBinderProvider());
options.ModelBinderProviders.Add(new ArrayModelBinderProvider());
options.ModelBinderProviders.Add(new CollectionModelBinderProvider());
options.ModelBinderProviders.Add(new ComplexObjectModelBinderProvider());
        // Set up ValueProviders
options.ValueProviderFactories.Add(new FormValueProviderFactory());
options.ValueProviderFactories.Add(new RouteValueProviderFactory());
options.ValueProviderFactories.Add(new QueryStringValueProviderFactory());
options.ValueProviderFactories.Add(new JQueryFormValueProviderFactory());
options.ValueProviderFactories.Add(new FormFileValueProviderFactory());

四、ModelBinderFactory

这个主要是实现  IModelBinderFactory 接口,此接口也要实现一个方法来创建 binder。

    IModelBinder CreateBinder(ModelBinderFactoryContext context);

内部默认的实现类为 ModelBinderFactory。这个 factory 是注册到依赖注入容器中的(单实例模式),它创建 binder 的依据是我们上面提到的各种 ModelBinderProvider,它就是调用了它们的 GetBinder 方法来获得 binder 的实例引用。

它会循环访问所有 provider,只要有一个 GetBinder 方法不返回 null,就算完成,然后就用这个 binder 来完成模型绑定。

        for (var i = 0; i < _providers.Length; i++)
{
var provider = _providers[i];
result = provider.GetBinder(providerContext);
if (result != null)
{
break
;
}

}

这个类在获取 binder 实例后会把 binder 缓存,当下一次再用到时就直接从缓存里面获取,免去了多次 new 的时间消耗。请大伙伴们记住它的缓存功能,因为后文咱们在实现 API 同时支持 JSON 和 Form 提交时会因为它导致问题。

五、实现同一个API支持 JSON 和 form-data 正文

前面四个标题都是准备理论,现在咱们才正式开始干活。

老周想要这样一个功能:假设某API是 /demo/product/edit,调用时要 POST 数据,然后被 Product 类型的参数接收。客户端使用 json 提交可以,使用 form-data 提交也可以。

要实现这个,可以用自定义 Binder 的方式。我们不需要自己编写复杂的 binder,因为内置的有现成的:

1、当调用 API 时提交的是 JSON,使用 BodyModelBinder 就可以读出内容;

2、当调用 API 时提交的是 form-data,使用 ComplexObjectModelBinder 就能读出。

综合上述两条,老周有个大胆的想法,于是付诸大胆的行动。咱们自己写个 ModelBinderProvider。

public class CustMtFmtBinderProvider : IModelBinderProvider
{
private readonly IModelBinderProvider bodybinderProvd;
private readonly IModelBinderProvider complexobjbinderProvd; public CustMtFmtBinderProvider(BodyModelBinderProvider bdp, ComplexObjectModelBinderProvider cmplxprd)
{
bodybinderProvd = bdp;
complexobjbinderProvd =
cmplxprd;
} public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
HttpContext httpctx = context.Services.GetRequiredService<IHttpContextAccessor>()?.HttpContext;
var request = httpctx.Request;
IModelBinder binder;
if(request.ContentType.StartsWith("multipart/form-data"))
{
binder = complexobjbinderProvd.GetBinder(context);
}
else
{
binder = bodybinderProvd.GetBinder(context);
}
return binder;
}
}

两个类型的 binder 可以通过构造函数来传,因为是用 MvcOptions 来添加的,不是依赖住入,所以我们可以自己传参。原理老周相信大伙能懂,就是判断 HTTP 请求的 Content-Type,如果是 multipart/form-data,就用复合类型的 binder,否则,用 Body binder,它默认支持JSON,不,是只支持JSON。

上一段 BodyModelBinder 的源代码:

    public async Task BindModelAsync(ModelBindingContext bindingContext)
{
……var formatter = (IInputFormatter?)null;
for (var i = 0; i < _formatters.Count; i++)
{
if (_formatters[i].CanRead(formatterContext))
{
formatter = _formatters[i];
……
break;
}
else
{
Log.InputFormatterRejected(_logger, _formatters[i], formatterContext);
}
} ……try
{
var result = await formatter.ReadAsync(formatterContext); if (result.HasError)
{
// Formatter encountered an error. Do not use the model it returned.
_logger.DoneAttemptingToBindModel(bindingContext);
return;
} ……
}

哟!这家伙并不是用 ValueProvider 来获取值的,而是使用 InputFormatter 读取的。上一篇水文中老周提过,当我们让控制器类用于 API 时,应用 [ApiController] 特性,它会把HTTP请求的整个 body 作为绑定源,并把模型绑定交给 InputFormatter 去处理。这里我们就看到真相了,就是 BodyModelBinder 搞的鬼,它调用了 Inputformatter。

Inputformatter 实现 IInputFormatter 接口,而运行库默认给应用注册的是 SystemTextJsonInputFormatter ,对于返回数据,则用的是 SystemTextJsonOutputFormatter 。关于返回的数据格式,老周前面也写过相关文章。

好了,回到主题,现在咱们的 BinderProvider 写好了,把它配置到 MvcOptions 对象中,成为全局的 binder 提供者。

var builder = WebApplication.CreateBuilder();
// 一定要注册这个
builder.Services.AddHttpContextAccessor();
builder.Services.AddControllers();
builder.Services.Configure<MvcOptions>(opt =>
{
BodyModelBinderProvider bp = opt.ModelBinderProviders.OfType<BodyModelBinderProvider>().FirstOrDefault()!;
ComplexObjectModelBinderProvider cp = opt.ModelBinderProviders.OfType<ComplexObjectModelBinderProvider>().FirstOrDefault()!;
opt.ModelBinderProviders.Insert(0, new CustMtFmtBinderProvider(bp, cp));
});
var app = builder.Build();

我们那个 Provider 中用到 HttpContext ,要在服务容器上调用 AddHttpContextAccessor 方法注册一下,不然会报错(因为在 Provider 中默认没有引用 Httpcontext 相关的对象,但 ModelBinder 的上下文中可以访问)。

嗯,思路是没错的,这 Job 看起来很完美。下面咱们定义个模型类。

public class Cat
{
public string Nickname { get; set; } = "";
public string? Category { get; set; }
public string Owner { get; set; } = "";
}

老周比较喜欢的两种动物:一种是喵喵,另一种是兔崽子。这里就定义个 Cat 类吧。

随后是 Controller。

[Route("cat")]
[ApiController]
public class CatController : Controller
{
[HttpPost]
[Route("new")]
[Consumes("application/json", "multipart/form-data")]
public string NewCat(Cat cc)
{
if (cc.Nickname == "")
return "你养了个寂寞"; string m = $"你新养了一只猫,它叫 {cc.Nickname}";
m += $"\n主人:{cc.Owner}\n品种:{cc.Category}";
return m;
}
}

Consumes 特性可以指定 API 方法支持哪些 content-type,当某个 action 有多个匹配方法时,还可以作为筛选方法的辅助依据。这里我就明确了两个类型—— application/json 和 multipart/form-data

然而,但是,意外,没想到,运行之后就让人傻眼了。果然理想是花容月貌,现实是鬼妖当道。问题如下:

A、运行后,选用 form-data,测试成功;但改为 JSON 提交就不行了;

B、运行后,选用 JSON 测试,成功;但改为 form-data 提交就不行了。

总结一下,就是运行后,第一次调用是正确的,无论是 JSON 还是 FORM 都是可以的,但之后再调用 API 就不正确了。其实咱们这个示例的思路是没有错的,但这时候你怎么 debug 都无法解决。问题出在哪呢?

前文老周提示了一下,记得乎?在介绍 ModelBinderFactory 时强调了一下,这家伙在调用某个 ModelBinderProvider 成功获得 ModelBinder 后会将其缓存。本示例的问题就是出在这儿了。缓存对象是字典类型(ConcurrentDictionary),Key 的组成要素之一(另一个可能是 ControllerParameterDescriptor,描述方法的参数信息)就是模型绑定的元数据(ModelMetadata),而元数据是从模型类型产生的。在这个示例中,模型无论要使用 BodyModelBinder 还是 ComplexObjectModelBinder,它的模型都是 Cat 类(也是一样的参数),因此元数据是不变的。

假设使用 JSON 方式提交,当第一次调用后,自定义 ModelBinderProvider 返回的 binder (假设叫 X)会被缓存;然后改为用 form data 提交,调用时,会优先从缓存中查找,然后找到 X,最后又用了 X 来进行模型绑定,于是就错了(此次应该用 Y)。

幸好,这个问题是可以解决的。咱们调整一下思路,先自定义一个 binder,在这个 binder 里封装 BodyModelBinder 和 ComplexObjectModelBinder 对象,然后在执行绑定时,再动态选择用 body 还是用 complexobject。

于是,示例做以下调整:

先编写一个自定义的 binder。

public class CustBinder : IModelBinder
{
private readonly BodyModelBinder _bodybinder;
private readonly ComplexObjectModelBinder _objectbinder; public CustBinder(BodyModelBinder bodyb, ComplexObjectModelBinder objb)
{
_bodybinder = bodyb;
_objectbinder
= objb;
} public async Task BindModelAsync(ModelBindingContext bindingContext)
{
// 通过 Content-Type 来区分
var request = bindingContext.HttpContext.Request;
if(request.ContentType!.StartsWith("multipart/form-data"))
{
await _objectbinder.BindModelAsync(bindingContext);
}
else
{
await _bodybinder.BindModelAsync(bindingContext);
}
}
}

这个自定义 Binder 中用到了另两个 Binder:Body 和 ComplexObject。通过构造函数两传递。在执行绑定时,通过请求的 ContentType 来判断,如果是 form-data,就用 ComplexObjectModelBinder,否则用 BodyModelBinder(直接调用它们的 BindModelAsync 方法就行了)。

补充:代码中在对象后面出现的“!”运算符,是告诉分析器“这个对象不会是null的,请放心”。在运行阶段无用处,只是在编译时不会有警告。

然后,再写一个 ModelBinderProvider。

public class CustFmtBinderProviderV2 : IModelBinderProvider
{
private readonly MvcOptions options;
public CustFmtBinderProviderV2(MvcOptions o)
{
options = o;
} public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context)); if (context.BindingInfo.BindingSource == null || context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body) == false)
return null; BodyModelBinderProvider bodyprvd = options.ModelBinderProviders.OfType<BodyModelBinderProvider>().FirstOrDefault()!;
ComplexObjectModelBinderProvider objprvd = options.ModelBinderProviders.OfType<ComplexObjectModelBinderProvider>().FirstOrDefault()!;
// 创建 binder 实例
IModelBinder? binder1 = bodyprvd.GetBinder(context);
IModelBinder? binder2 = objprvd.GetBinder(context);
// 两个 binder 都要用到,均不能为 null
if(binder1 != null && binder2 != null)
{
return new CustBinder((BodyModelBinder)binder1, (ComplexObjectModelBinder)binder2);
}
return null;
}
}

BodyModelBinderProvider 和 ComplexObjectModelBinderProvider 从 MvcOptions 对象的 ModelBinderProviders 列表中获取。创建 CustBinder 实例时,把这两个 binder 传给它的构造函数。

经过这样处理之后,被 ModelBinderFactory 缓存的是 CustBinder 实例,哪怕在第二次以上调用时,都能正确进行 Content-Type 的分析,因为筛选的代码是写在 CustBinder 内部的,就算调用的是已缓存的实例也不影响其逻辑。

最后,Program.cs 文件那里也改一下。

var builder = WebApplication.CreateBuilder();
builder.Services.AddControllers();
builder.Services.Configure<MvcOptions>(opt =>
{
opt.ModelBinderProviders.Insert(0, new CustFmtBinderProviderV2(opt));
});
var app = builder.Build();

测试一下,运行后,先以 form-data 输入。

POST /cat/new HTTP/1.1
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Host: localhost:5168
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------031532983200066969593455
Content-Length: 395 ----------------------------031532983200066969593455
Content-Disposition: form-data; name="nickname"
豆豆
----------------------------031532983200066969593455
Content-Disposition: form-data; name="owner"
小王
----------------------------031532983200066969593455
Content-Disposition: form-data; name="category"
大狸花
----------------------------031532983200066969593455-- HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Thu, 24 Mar 2022 08:57:14 GMT
Server: Kestrel
Transfer-Encoding: chunked 你新养了一只猫,它叫 豆豆
主人:小王
品种:大狸花

通过,没问题。

接着,改为用 JSON 方式提交。

POST /cat/new HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Host: localhost:5168
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 84 {
"nickname": "豆豆",
"category": "大橘",
"owner": "赛冬瓜"
} HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Thu, 24 Mar 2022 08:58:57 GMT
Server: Kestrel
Transfer-Encoding: chunked 你新养了一只猫,它叫 豆豆
主人:赛冬瓜
品种:大橘

嗯嗯嗯,效果不错吧。现在这个 API 既可以用 form-data 输入数据,也能用 JSON 输入数据了。咱们就不必把同一个 API 写两个版本了。

【ASP.NET Core】MVC模型绑定——实现同一个API方法兼容JSON和Form-data输入的更多相关文章

  1. ASP.NET Core MVC 模型绑定用法及原理

    前言 查询了一下关于 MVC 中的模型绑定,大部分都是关于如何使用的,以及模型绑定过程中的一些用法和概念,很少有关于模型绑定的内部机制实现的文章,本文就来讲解一下在 ASP.NET Core MVC ...

  2. ASP.NET Core MVC 模型绑定 (转载)

    ASP.NET Core MVC的Model Binding会将HTTP Request数据,以映射的方式对应到参数中.基本上跟ASP.NET MVC差不多,但能Binding的来源更多了一些.本篇将 ...

  3. .net core mvc 模型绑定 之 json and urlencoded

    .net core mvc 模型绑定, FromQuery,对应 url 中的 urlencoded string ("?key1=value1&key2=value2") ...

  4. 为什么 web 开发人员需要迁移到. NET Core, 并使用 ASP.NET Core MVC 构建 web 和 webservice/API

    2018 .NET开发者调查报告: .NET Core 是怎么样的状态,这里我们看到了还有非常多的.net开发人员还在观望,本文给大家一个建议.这仅代表我的个人意见, 我有充分的理由推荐.net 程序 ...

  5. ASP.NET Core 中文文档 第二章 指南(2)用 Visual Studio 和 ASP.NET Core MVC 创建首个 Web API

    原文:Building Your First Web API with ASP.NET Core MVC and Visual Studio 作者:Mike Wasson 和 Rick Anderso ...

  6. 在ASP.NET Core MVC中构建简单 Web Api

    Getting Started 在 ASP.NET Core MVC 框架中,ASP.NET 团队为我们提供了一整套的用于构建一个 Web 中的各种部分所需的套件,那么有些时候我们只需要做一个简单的 ...

  7. 【翻译】在Visual Studio中使用Asp.Net Core MVC创建第一个Web Api应用(二)

    运行应用 In Visual Studio, press CTRL+F5 to launch the app. Visual Studio launches a browser and navigat ...

  8. ASP.NET Core 中文文档 第四章 MVC(01)ASP.NET Core MVC 概览

    原文:Overview of ASP.NET Core MVC 作者:Steve Smith 翻译:张海龙(jiechen) 校对:高嵩 ASP.NET Core MVC 是使用模型-视图-控制器(M ...

  9. ASP.NET Core MVC 概述

    https://docs.microsoft.com/zh-cn/aspnet/core/mvc/overview?view=aspnetcore-2.2 ASP.NET Core MVC 概述 20 ...

随机推荐

  1. swpu新生赛ctf wp

    WEB:gift_F12 没啥好说的 直接F12得了 NSSCTF{We1c0me_t0_WLLMCTF_Th1s_1s_th3_G1ft} RE 简简单单的解密 import base64, url ...

  2. 关于git和SVN的介绍和区别

    主要对git,svn进行一个简单的介绍. 顺带,我会在后面把我整理的一整套CSS3,PHP,MYSQL的开发的笔记打包放到百度云,有需要可以直接去百度云下载,这样以后你们开发就可以直接翻笔记不用百度搜 ...

  3. 02编程语言与python介绍

    编程语言分类 机器语言:直接用计算机能理解的二进制指令去编写程序,是直接在控制计算机硬件 优点:运行效率高 缺点:开发效率低 1.开发一个简单的小功能都要哟个到非常多条数的二进制指令 2.二进制指令非 ...

  4. Solution -「UR #2」「UOJ #32」跳蚤公路

    \(\mathcal{Description}\)   Link.   给定一个 \(n\) 个点 \(m\) 条边的带权有向图,每条边还有属性 \(s\in\{-1,0,1\}\).对于每个 \(u ...

  5. 通过PEB寻找函数地址

      通过PEB的Ldr参数(结构体定义为_PEB_LDR_DATA),遍历当前进程加载的模块信息链表,找到目标模块.   摘自PEB LDR DATA: typedef struct _PEB_LDR ...

  6. Java并发基础之AbstractQueuedSynchronizer(AQS)

    AbstractQueuedSynchronizer同步器是实现JUC核心基础组件,因为 定义了一套多线程访问共享资源的同步器框架.前面几篇文章中JUC同步工具中都利用AQS构建自身的阻塞类.AQS解 ...

  7. JVM学习——字节码(学习过程)

    JVM--字节码 为什么要学字节码 字节码文件,有什么用? JVM虚拟机的特点:一处编译,多处运行. 多处运行,靠的是.class 字节码文件. JVM本身,并不是跨平台的.Java之所以跨平台,是因 ...

  8. [WPF] 使用 Effect 玩玩阴影、内阴影、 长阴影

    最近在学习怎么用 Shazzam Shader Editor 编写自定义的 Effect,并试着去实现阴影.内阴影和长阴影的效果.结果我第一步就放弃了,因为阴影用到的高斯模糊算法对我来说太太太太太太太 ...

  9. jmeter变量嵌套:__V

    问题复现 ${name_${n}} 下面没有获取到结果 解决方案 __V是用于执行变量名表达式 ${__V(name_${n})} 获取到结果

  10. 我遇到移动端ios系统遇到的一些坑和解决办法

    我是作为一个H5移动端开发.主要是做跨平台兼容ios系统和Android系统. 在移动端中,最让我头疼的不是功能,不是业务逻辑.而是适配.俗话说:移动端适配是最头疼的事情,也是头发掉得最快的时候. 我 ...