[转]Creating an OData v3 Endpoint with Web API 2
by Mike Wasson+
The Open Data Protocol (OData) is a data access protocol for the web. OData provides a uniform way to structure data, query the data, and manipulate the data set through CRUD operations (create, read, update, and delete). OData supports both AtomPub (XML) and JSON formats. OData also defines a way to expose metadata about the data. Clients can use the metadata to discover the type information and relationships for the data set.+
ASP.NET Web API makes it easy to create an OData endpoint for a data set. You can control exactly which OData operations the endpoint supports. You can host multiple OData endpoints, alongside non-OData endpoints. You have full control over your data model, back-end business logic, and data layer.+
Software versions used in the tutorial
- Visual Studio 2013
- Web API 2
- OData Version 3
- Entity Framework 6
- Fiddler Web Debugging Proxy (Optional)
Web API OData support was added in ASP.NET and Web Tools 2012.2 Update. However, this tutorial uses scaffolding that was added in Visual Studio 2013.+
In this tutorial, you will create a simple OData endpoint that clients can query. You will also create a C# client for the endpoint. After you complete this tutorial, the next set of tutorials show how to add more functionality, including entity relations, actions, and $expand/$select.+
Create the Visual Studio Project
In this tutorial, you will create an OData endpoint that supports basic CRUD operations. The endpoint will expose a single resource, a list of products. Later tutorials will add more features.+
Start Visual Studio and select New Project from the Start page. Or, from the File menu, select New and then Project.+
In the Templates pane, select Installed Templates and expand the Visual C# node. Under Visual C#, select Web. Select the ASP.NET Web Application template.+
In the New ASP.NET Project dialog, select the Empty template. Under "Add folders and core references for...", check Web API. Click OK.+
Add an Entity Model
A model is an object that represents the data in your application. For this tutorial, we need a model that represents a product. The model corresponds to our OData entity type.+
In Solution Explorer, right-click the Models folder. From the context menu, select Add then select Class.+
In the Add New Item dialog, name the class "Product".+
Note
By convention, model classes are placed in the Models folder. You don't have to follow this convention in your own projects, but we'll use it for this tutorial.+
In the Product.cs file, add the following class definition:+
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
The ID property will be the entity key. Clients can query products by ID. This field would also be the primary key in the back-end database.+
Build the project now. In the next step, we'll use some Visual Studio scaffolding that uses reflection to find the Product type.+
Add an OData Controller
A controller is a class that handles HTTP requests. You define a separate controller for each entity set in you OData service. In this tutorial, we'll create a single controller.+
In Solution Explorer, right-click the the Controllers folder. Select Add and then select Controller.+
In the Add Scaffold dialog, select "Web API 2 OData Controller with actions, using Entity Framework".+
In the Add Controller dialog, name the controller "ProductsController". Select the "Use async controller actions" checkbox. In the Model drop-down list, select the Product class.+
Click the New data context... button. Leave the default name for the data context type, and click Add.+
Click Add in the Add Controller dialog to add the controller.+
Note: If you get an error message that says "There was an error getting the type...", make sure that you built the Visual Studio project after you added the Product class. The scaffolding uses reflection to find the class.+
The scaffolding adds two code files to the project:+
- Products.cs defines the Web API controller that implements the OData endpoint.
- ProductServiceContext.cs provides methods to query the underlying database, using Entity Framework.
Add the EDM and Route
In Solution Explorer, expand the App_Start folder and open the file named WebApiConfig.cs. This class holds configuration code for Web API. Replace this code with the following:+
using ProductService.Models;
using System.Web.Http;
using System.Web.Http.OData.Builder;
namespace ProductService
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
}
}
}
This code does two things:+
- Creates an Entity Data Model (EDM) for the OData endpoint.
- Adds a route for the endpoint.
An EDM is an abstract model of the data. The EDM is used to create the metadata document and define the URIs for the service. The ODataConventionModelBuilder creates an EDM by using a set of default naming conventions EDM. This approach requires the least code. If you want more control over the EDM, you can use the ODataModelBuilder class to create the EDM by adding properties, keys, and navigation properties explicitly.+
The EntitySet method adds an entity set to the EDM:+
modelBuilder.EntitySet<Product>("Products");
The string "Products" defines the name of the entity set. The name of the controller must match the name of the entity set. In this tutorial, the entity set is named "Products" and the controller is named ProductsController. If you named the entity set "ProductSet", you would name the controller ProductSetController. Note that an endpoint can have multiple entity sets. Call EntitySet<T> for each entity set, and then define a corresponding controller.+
The MapODataRoute method adds a route for the OData endpoint.+
config.Routes.MapODataRoute("ODataRoute", "odata", model);
The first parameter is a friendly name for the route. Clients of your service do not see this name. The second parameter is the URI prefix for the endpoint. Given this code, the URI for the Products entity set is http://hostname/odata/Products. Your application can have more than one OData endpoint. For each endpoint, call MapODataRoute and provide a unique route name and a unique URI prefix.+
Seed the Database (Optional)
In this step, you will use Entity Framework to seed the database with some test data. This step is optional, but it lets you test out your OData endpoint right away.+
From the Tools menu, select Library Package Manager, then select Package Manager Console. In the Package Manager Console window, enter the following command:+
Enable-Migrations
This adds a folder named Migrations and a code file named Configuration.cs.+
Open this file and add the following code to the Configuration.Seed method.+
protected override void Seed(ProductService.Models.ProductServiceContext context)
{
// New code
context.Products.AddOrUpdate(new Product[] {
new Product() { ID = 1, Name = "Hat", Price = 15, Category = "Apparel" },
new Product() { ID = 2, Name = "Socks", Price = 5, Category = "Apparel" },
new Product() { ID = 3, Name = "Scarf", Price = 12, Category = "Apparel" },
new Product() { ID = 4, Name = "Yo-yo", Price = 4.95M, Category = "Toys" },
new Product() { ID = 5, Name = "Puzzle", Price = 8, Category = "Toys" },
});
}
In the Package Manager Console Window, enter the following commands:+
Add-Migration Initial
Update-Database
These commands generate code that creates the database, and then executes that code.+
Exploring the OData Endpoint
In this section, we'll use the Fiddler Web Debugging Proxy to send requests to the endpoint and examine the response messages. This will help you to understand the capabilities of an OData endpoint.+
In Visual Studio, press F5 to start debugging. By default, Visual Studio opens your browser to http://localhost:*port*, where port is the port number configured in the project settings.+
You can change the port number in the project settings. In Solution Explorer, right-click the project and select Properties. In the properties window, select Web. Enter the port number under Project Url.+
Service Document
The service document contains a list of the entity sets for the OData endpoint. To get the service document, send a GET request to the root URI of the service.+
Using Fiddler, enter the following URI in the Composer tab: http://localhost:port/odata/, where port is the port number.+
Click the Execute button. Fiddler sends an HTTP GET request to your application. You should see the response in the Web Sessions list. If everything is working, the status code will be 200.+
Double-click the response in the Web Sessions list to see the details of the response message in the Inspectors tab.+
The raw HTTP response message should look similar to the following:+
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/atomsvc+xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 17:51:01 GMT
Content-Length: 364
<?xml version="1.0" encoding="utf-8"?>
<service xml:base="http://localhost:60868/odata"
xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom">
<workspace>
<atom:title type="text">Default</atom:title>
<collection href="Products">
<atom:title type="text">Products</atom:title>
</collection>
</workspace>
</service></pre>
By default, Web API returns the service document in AtomPub format. To request JSON, add the following header to the HTTP request:+
Accept: application/json+
Now the HTTP response contains a JSON payload:+
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 22:59:28 GMT
Content-Length: 136
{
"odata.metadata":"http://localhost:60868/odata/$metadata","value":[
{
"name":"Products","url":"Products"
}
]
}
Service Metadata Document
The service metadata document describes the data model of the service, using an XML language called the Conceptual Schema Definition Language (CSDL). The metadata document shows the structure of the data in the service, and can be used to generate client code.+
To get the metadata document, send a GET request to http://localhost:port/odata/$metadata. Here is the metadata for the endpoint shown in this tutorial.+
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:05:52 GMT
Content-Length: 1086
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
<edmx:DataServices m:DataServiceVersion="3.0" m:MaxDataServiceVersion="3.0"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<Schema Namespace="ProductService.Models" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityType Name="Product">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Name="ID" Type="Edm.Int32" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
<Property Name="Price" Type="Edm.Decimal" Nullable="false" />
<Property Name="Category" Type="Edm.String" />
</EntityType>
</Schema>
<Schema Namespace="Default" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityContainer Name="Container" m:IsDefaultEntityContainer="true">
<EntitySet Name="Products" EntityType="ProductService.Models.Product" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Entity Set
To get the Products entity set, send a GET request to http://localhost:port/odata/Products.+
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:01:31 GMT
Content-Length: 459
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products","value":[
{
"ID":1,"Name":"Hat","Price":"15.00","Category":"Apparel"
},{
"ID":2,"Name":"Socks","Price":"5.00","Category":"Apparel"
},{
"ID":3,"Name":"Scarf","Price":"12.00","Category":"Apparel"
},{
"ID":4,"Name":"Yo-yo","Price":"4.95","Category":"Toys"
},{
"ID":5,"Name":"Puzzle","Price":"8.00","Category":"Toys"
}
]
}
Entity
To get an individual product, send a GET request to http://localhost:port/odata/Products(1), where "1" is the product ID.+
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:04:29 GMT
Content-Length: 140
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products/@Element","ID":1,
"Name":"Hat","Price":"15.00","Category":"Apparel"
}
OData Serialization Formats
OData supports several serialization formats:+
- Atom Pub (XML)
- JSON "light" (introduced in OData v3)
- JSON "verbose" (OData v2)
By default, Web API uses AtomPubJSON "light" format. +
To get AtomPub format, set the Accept header to "application/atom+xml". Here is an example response body:+
<?xml version="1.0" encoding="utf-8"?>
<entry xml:base="http://localhost:60868/odata" xmlns="http://www.w3.org/2005/Atom" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
<id>http://localhost:60868/odata/Products(1)</id>
<category term="ProductService.Models.Product" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<link rel="edit" href="http://localhost:60868/odata/Products(1)" />
<link rel="self" href="http://localhost:60868/odata/Products(1)" />
<title />
<updated>2013-09-23T23:42:11Z</updated>
<author>
<name />
</author>
<content type="application/xml">
<m:properties>
<d:ID m:type="Edm.Int32">1</d:ID>
<d:Name>Hat</d:Name>
<d:Price m:type="Edm.Decimal">15.00</d:Price>
<d:Category>Apparel</d:Category>
</m:properties>
</content>
</entry>
You can see one obvious disadvantage of the Atom format: It's a lot more verbose than the JSON light. However, if you have a client that understands AtomPub, the client might prefer that format over JSON.+
Here is the JSON light version of the same entity:+
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products/@Element",
"ID":1,
"Name":"Hat",
"Price":"15.00",
"Category":"Apparel"
}
The JSON light format was introduced in version 3 of the OData protocol. For backward compatibility, a client can request the older "verbose" JSON format. To request verbose JSON, set the Accept header to application/json;odata=verbose. Here is the verbose version:+
{
"d":{
"__metadata":{
"id":"http://localhost:18285/odata/Products(1)",
"uri":"http://localhost:18285/odata/Products(1)",
"type":"ProductService.Models.Product"
},"ID":1,"Name":"Hat","Price":"15.00","Category":"Apparel"
}
}
This format conveys more metadata in the response body, which can add considerable overhead over an entire session. Also, it adds a level of indirection by wrapping the object in a property named "d".+
Next Steps
using Microsoft.Data.OData;
using Microsoft.Data.OData.Query;
using ProductService.Models;
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using System.Web.Http.OData;
using System.Web.Http.OData.Routing;
using System.Web.Http.Routing; namespace ProductService.Controllers
{
public class ProductsController : ODataController
{
private ProductServiceContext db = new ProductServiceContext(); // GET odata/Products
[Queryable]
public IQueryable<Product> GetProducts()
{
return db.Products;
} // GET odata/Products(5)
[Queryable]
public SingleResult<Product> GetProduct([FromODataUri] int key)
{
return SingleResult.Create(db.Products.Where(product => product.ID == key));
} // PUT odata/Products(5)
public async Task<IHttpActionResult> Put([FromODataUri] int key, Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
} if (key != product.ID)
{
return BadRequest();
} db.Entry(product).State = EntityState.Modified; try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ProductExists(key))
{
return NotFound();
}
else
{
throw;
}
} return Updated(product);
} // POST odata/Products
public async Task<IHttpActionResult> Post(Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
} db.Products.Add(product);
await db.SaveChangesAsync(); return Created(product);
} // PATCH odata/Products(5)
[AcceptVerbs("PATCH", "MERGE")]
public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> patch)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
} Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
} patch.Patch(product); try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ProductExists(key))
{
return NotFound();
}
else
{
throw;
}
} return Updated(product);
} // DELETE odata/Products(5)
public async Task<IHttpActionResult> Delete([FromODataUri] int key)
{
Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
} db.Products.Remove(product);
await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent);
} // GET /Products(1)/Supplier
public Supplier GetSupplier([FromODataUri] int key)
{
Product product = db.Products.FirstOrDefault(p => p.ID == key);
if (product == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return product.Supplier;
} protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
} private bool ProductExists(int key)
{
return db.Products.Count(e => e.ID == key) > ;
} // The next methods support entity relations.
// For more information, see http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/working-with-entity-relations public async Task<IHttpActionResult> CreateLink([FromODataUri] int key, string navigationProperty, [FromBody] Uri link)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
} Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
} switch (navigationProperty)
{
case "Supplier":
string supplierKey = GetKeyFromLinkUri<string>(link);
Supplier supplier = await db.Suppliers.FindAsync(supplierKey);
if (supplier == null)
{
return NotFound();
}
product.Supplier = supplier;
await db.SaveChangesAsync();
return StatusCode(HttpStatusCode.NoContent); default:
return NotFound();
}
} public async Task<IHttpActionResult> DeleteLink([FromODataUri] int key, string navigationProperty)
{
Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
} switch (navigationProperty)
{
case "Supplier":
product.Supplier = null;
await db.SaveChangesAsync();
return StatusCode(HttpStatusCode.NoContent); default:
return NotFound(); }
} // Helper method to extract the key from an OData link URI.
private TKey GetKeyFromLinkUri<TKey>(Uri link)
{
TKey key = default(TKey); // Get the route that was used for this request.
IHttpRoute route = Request.GetRouteData().Route; // Create an equivalent self-hosted route.
IHttpRoute newRoute = new HttpRoute(route.RouteTemplate,
new HttpRouteValueDictionary(route.Defaults),
new HttpRouteValueDictionary(route.Constraints),
new HttpRouteValueDictionary(route.DataTokens), route.Handler); // Create a fake GET request for the link URI.
var tmpRequest = new HttpRequestMessage(HttpMethod.Get, link); // Send this request through the routing process.
var routeData = newRoute.GetRouteData(
Request.GetConfiguration().VirtualPathRoot, tmpRequest); // If the GET request matches the route, use the path segments to find the key.
if (routeData != null)
{
ODataPath path = tmpRequest.GetODataPath();
var segment = path.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
if (segment != null)
{
// Convert the segment into the key type.
key = (TKey)ODataUriUtils.ConvertFromUriLiteral(
segment.Value, ODataVersion.V3);
}
}
return key;
} // The next method implements an OData action.
// For more information, see http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-actions // POST /odata/Products(5)/RateProduct
[HttpPost]
public async Task<IHttpActionResult> RateProduct([FromODataUri] int key, ODataActionParameters parameters)
{
if (!ModelState.IsValid)
{
return BadRequest();
} int rating = (int)parameters["Rating"]; Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
} product.Ratings.Add(new ProductRating() { Rating = rating });
db.SaveChanges(); double average = product.Ratings.Average(x => x.Rating); return Ok(average);
} }
}
[转]Creating an OData v3 Endpoint with Web API 2的更多相关文章
- AngularJS使用OData请求ASP.NET Web API资源的思路
本篇整理AngularJS使用OData请求ASP.NET Web API资源的思路. 首先给ASP.NET Web API插上OData的翅膀,通过NuGet安装OData. 然后,给control ...
- Creating Help Pages for ASP.NET Web API -摘自网络
When you create a web API, it is often useful to create a help page, so that other developers will k ...
- OData查询ASP.NET Web API全攻略
本篇使用ASP.NET Web API来体验OData各种query. 首先是本篇即将用到的Model.使用的OData版本是4.0. public class Customer { public i ...
- Create an OData v4 Endpoint Using ASP.NET Web API 2.2(使用ASP.NET Web API 2.2创建OData v4端点)
开放数据协议Open Data Protocol(OData)是web的一种数据存取协议,OData通过设置CRUD操作(Create创建.Read读取.Update更新,Delete删除)提供一种统 ...
- 对一个前端AngularJS,后端OData,ASP.NET Web API案例的理解
依然chsakell,他写了一篇前端AngularJS,后端OData,ASP.NET Web API的Demo,关于OData在ASP.NET Web API中的正删改查没有什么特别之处,但在前端调 ...
- Web API 2
Asp.Net Web API 2 官网菜鸟学习系列导航[持续更新中] 前言 本来一直参见于微软官网进行学习的, 官网网址http://www.asp.net/web-api.出于自己想锻炼一下学 ...
- 杂项:ASP.NET Web API
ylbtech-杂项:ASP.NET Web API ASP.NET Web API 是一种框架,用于轻松构建可以访问多种客户端(包括浏览器和移动设备)的 HTTP 服务. ASP.NET Web A ...
- ASP.NET Web API系列教程目录
ASP.NET Web API系列教程目录 Introduction:What's This New Web API?引子:新的Web API是什么? Chapter 1: Getting Start ...
- ASP.NET Web API系列教程(目录)(转)
注:微软随ASP.NET MVC 4一起还发布了一个框架,叫做ASP.NET Web API.这是一个用来在.NET平台上建立HTTP服务的Web API框架,是微软的又一项令人振奋的技术.目前,国内 ...
随机推荐
- pyppeteer初尝滋味
最近在爬几个电商平台网站用的selenium一登录就会有验证,目前这些网站对selenium检测很严格 因为不少大网站有对selenium的js监测机制.比如navigator.webdriver,n ...
- 【OCP 12c】最新CUUG OCP-071考试题库(64题)
64.(22-7) choose the best answer: View the Exhibit and examine the structure of the ORDERS and ORDER ...
- “全栈2019”Java第四章:创建第一个Java程序
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- Java_IO流输入输出
第三章 输入输出 一.I/O Input/Output 二.File 用途:对文件和目录进行常规操作(除文件读写操作外). 方法:exists():判断文件或目录是否存在 isFile():判断是否是 ...
- php 递归数据,三维数组转换二维
public function sortarea($area, $parent_id = 0, $lev = 1){ static $list; foreach($area as $v){ if($v ...
- [XHR]——重新认识 XMLHttpRequest
细说XMLHttpRequest如何使用 先来看一段使用XMLHttpRequest发送Ajax请求的简单示例代码. function sendAjax() { //构造表单数据 var formDa ...
- Spring aop+自定义注解统一记录用户行为日志
写在前面 本文不涉及过多的Spring aop基本概念以及基本用法介绍,以实际场景使用为主. 场景 我们通常有这样一个需求:打印后台接口请求的具体参数,打印接口请求的最终响应结果,以及记录哪个用户在什 ...
- 《条目二十九:对于逐个字符的输入请考虑istreambuf_iterator》
<条目二十九:对于逐个字符的输入请考虑istreambuf_iterator> 1.使用: ifstream inputfile("xxxx"); string fil ...
- QuantLib 金融计算——数学工具之求解器
目录 QuantLib 金融计算--数学工具之求解器 概述 调用方式 非 Newton 算法(不需要导数) Newton 算法(需要导数) 如果未做特别说明,文中的程序都是 Python3 代码. Q ...
- 一个好用的ssh终端:MobaXterm
WSL由于没有图形界面,所有操作都是在命令行里执行,平时用来编译和跑CFD代码其实还是挺方便.不过有时候要查看WSL里的文件就比较麻烦,这时可以用SFTP这类工具,连接过去后直接操作文件.试过几个这类 ...















