Back to "خط الطلب والاستجابة الأساسيان - الجزء 4: المسار والنقاط النهاية"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI-Article ASP.NET ASP.NET Lifecycle

خط الطلب والاستجابة الأساسيان - الجزء 4: المسار والنقاط النهاية

Sunday, 09 November 2025

أولاً

لقد سافرنا من خلال طبقة الاستضافة، استكشفنا Kestrel، واتقننا البرمجيات المتوسطة. الآن وصلنا إلى سؤال حاسم: مرة واحدة في الطلب (المسار في) وقد عقد مؤتمراً بشأن نقاط النهاية.

التوجيه هو الآلية التي تطابق طلبات HTTP للشفرة القابلة للتنفيذ. نقاط النهاية هي الوجهات - الشفرة الفعلية التي تنفذ. معاً، تشكل نظاماً قوياً ومرناً يدعم كل شيء من معالجات الطرق البسيطة إلى تطبيقات MVC المعقدة.

في هذا الجزء، سنستكشف كيف يعمل التوجيه، كيف يتم تسجيل نقاط النهاية وتنفيذها، وكيف يمكنك الاستفادة من هذا النظام لبناء تطبيقات متطورة.

ملاحظة: هذا جزء من تجاربي مع AI/ طريقة لإنفاق 1000 دولار كرمز ائتمانات الويب. لقد أطعمت هذه مجموعة من الأوراق، فهمي، أسئلة كان علي توليد هذه المقالة. إنها ممتعة وتملأ فجوة لم أرها ملأها في أي مكان آخر.

تطور المسار

وقد تطور توجيه الشبكة الأساسية تطوراً كبيراً:

timeline
    title ASP.NET Core Routing Evolution
    ASP.NET Core 1.0 - 2.2 : Traditional Routing : Routes defined in middleware : MapRoute for MVC
    ASP.NET Core 3.0 - 3.1 : Endpoint Routing : UseRouting + UseEndpoints : Routing metadata aware
    ASP.NET Core 6.0+ : Minimal APIs : Direct MapGet/MapPost : Simplified registration

التوجيه التقليدي (قبل 3.3 في المائة):

app.UseMvc(routes =>
{
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

مسار نقطة النهاية (3.0+):

app.UseRouting();
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

الحد الأدنى الأدنى المعاصر لمؤشرات القدرة الشرائية (6-+):

app.MapGet("/users/{id}", (int id) => $"User {id}");

الطريقة التي يعمل بها التدريب: عملية المرحلتين

يعمل مسار نقطة النهاية على مرحلتين:

graph LR
    A[Request] --> B[Routing Middleware]
    B --> C{Match Route?}
    C -->|Yes| D[Set Endpoint]
    C -->|No| E[No Endpoint Set]
    D --> F[Authorization Middleware]
    E --> F
    F --> G[Other Middleware]
    G --> H[Endpoint Middleware]
    H --> I{Endpoint Exists?}
    I -->|Yes| J[Execute Endpoint]
    I -->|No| K[404 Not Found]

    style B stroke:#ef4444,stroke-width:3px
    style H stroke:#10b981,stroke-width:3px
    style J stroke:#6366f1,stroke-width:3px

المرحلة 1:UseRouting())

يتم فحص الطلب ويحاول مضاهاته بنقطة نهاية مسجلة:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Phase 1: Route matching happens here
app.UseRouting();

// Between routing and endpoints, you can use middleware that needs route information
app.Use(async (context, next) =>
{
    var endpoint = context.GetEndpoint();
    if (endpoint != null)
    {
        Console.WriteLine($"Matched endpoint: {endpoint.DisplayName}");

        // Access route values
        var routeValues = context.Request.RouteValues;
        foreach (var (key, value) in routeValues)
        {
            Console.WriteLine($"  {key} = {value}");
        }
    }

    await next(context);
});

app.UseAuthorization(); // Can make decisions based on matched endpoint

// Phase 2: Endpoint execution happens here
app.MapGet("/users/{id}", (int id) => $"User {id}");

app.Run();

الرسم البياني:

sequenceDiagram
    participant Request
    participant Routing as UseRouting
    participant Auth as UseAuthorization
    participant Endpoint as Endpoint Middleware

    Request->>Routing: GET /users/123
    Note over Routing: Parse URL<br/>Match against patterns<br/>Extract route values
    Routing->>Routing: Found match: /users/{id}
    Routing->>Routing: Set endpoint metadata<br/>Set route values {id: 123}

    Routing->>Auth: Continue with endpoint set
    Note over Auth: Can check endpoint metadata<br/>for [Authorize] attributes

    Auth->>Endpoint: Continue
    Note over Endpoint: Endpoint is set
    Endpoint->>Endpoint: Execute matched endpoint<br/>with route values

    Endpoint->>Request: Return response

المرحلة 2: التنفيذ عند نقطة النهاية

الـ نقطة النهاية نقطة الـ نقطة النهاية نقطة النهاية نقطة:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseRouting();

// This middleware runs BEFORE endpoint execution
app.Use(async (context, next) =>
{
    Console.WriteLine("Before endpoint execution");
    await next(context);
    Console.WriteLine("After endpoint execution");
});

// Endpoint execution happens here
app.MapGet("/", () =>
{
    Console.WriteLine("Executing endpoint");
    return "Hello World";
});

app.Run();

الناتج:

Before endpoint execution
Executing endpoint
After endpoint execution

& & &

حدّد سِنّة سِنّة سِنّة سِنّة سِنّة سِنّة سِنّة سِنّة سِنّة سِنّة سِنّة:

& & & & &

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Literal path
app.MapGet("/", () => "Home page");

// Single parameter
app.MapGet("/users/{id}", (int id) => $"User {id}");

// Multiple parameters
app.MapGet("/posts/{year}/{month}/{day}", (int year, int month, int day) =>
    $"Posts from {year}-{month:D2}-{day:D2}");

// Optional parameter
app.MapGet("/products/{id?}", (int? id) =>
    id.HasValue ? $"Product {id}" : "All products");

// Default value
app.MapGet("/search/{query=all}", (string query) =>
    $"Searching for: {query}");

// Catch-all parameter
app.MapGet("/files/{*path}", (string path) =>
    $"File path: {path}");

app.Run();

أمثلة:

GET /                           → "Home page"
GET /users/123                  → "User 123"
GET /posts/2024/1/15            → "Posts from 2024-01-15"
GET /products                   → "All products"
GET /products/42                → "Product 42"
GET /search                     → "Searching for: all"
GET /search/aspnet              → "Searching for: aspnet"
GET /files/docs/guide.pdf       → "File path: docs/guide.pdf"

الطريق

(أ) ضمان تطابق البارامترات مع أنماط محددة:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Integer constraint
app.MapGet("/users/{id:int}", (int id) =>
    $"User {id}");

// Minimum value
app.MapGet("/products/{id:int:min(1)}", (int id) =>
    $"Product {id}");

// Range
app.MapGet("/items/{id:int:range(1,100)}", (int id) =>
    $"Item {id}");

// String length
app.MapGet("/codes/{code:length(5)}", (string code) =>
    $"Code {code}");

// Min/max length
app.MapGet("/names/{name:minlength(2):maxlength(20)}", (string name) =>
    $"Name {name}");

// Regex
app.MapGet("/posts/{slug:regex(^[a-z0-9-]+$)}", (string slug) =>
    $"Post slug: {slug}");

// GUID
app.MapGet("/orders/{id:guid}", (Guid id) =>
    $"Order {id}");

// DateTime
app.MapGet("/appointments/{date:datetime}", (DateTime date) =>
    $"Appointment on {date:yyyy-MM-dd}");

// Alpha (letters only)
app.MapGet("/tags/{tag:alpha}", (string tag) =>
    $"Tag: {tag}");

// Multiple constraints (AND)
app.MapGet("/archive/{year:int:min(2000):max(2030)}", (int year) =>
    $"Archive for {year}");

app.Run();

& & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & &:

ضابط الضابط مثال مباريات |------------|---------|---------| | int | {id:int} | 123, -456 | | bool | {active:bool} □ صحيح ، خاطئ / / | datetime | {date:datetime} | 2024-01-15 | | decimal | {price:decimal} | 19.99 | | double | {lat:double} | 51.5074 | | float | {temp:float} | 98.6 | | guid | {id:guid} 550e8400-e29b-41d4-a716-446655440000 | long | {size:long} | 9223372036854775807 | | minlength(n) | {name:minlength(3)} o abc, abcd | maxlength(n) | {name:maxlength(5)} o abc, abcd | length(n) | {code:length(5)} o abcde oth | min(n) | {age:min(18)} | 18, 19, 100 | | max(n) | {age:max(120)} | 1, 100, 120 | | range(min,max) | {num:range(1,10)} | 1, 5, 10 | | alpha | {tag:alpha} o abc, XYZ / abc, XYZ | regex(pattern) | {code:regex(^[A-Z]{3}$)} ABC، XYZ | required | {id:required} □ أي قيمة غير معفاة

(أ) القيود:

# ✅ Matches {id:int}
GET /users/123          → Success

# ❌ Doesn't match {id:int}
GET /users/abc          → 404

# ✅ Matches {id:int:min(1)}
GET /products/5         → Success

# ❌ Doesn't match {id:int:min(1)}
GET /products/0         → 404
GET /products/-1        → 404

أنشئ قيودك الخاصة:

// Custom constraint: validates slugs
public class SlugConstraint : IRouteConstraint
{
    private readonly Regex _regex = new Regex(@"^[a-z0-9]+(?:-[a-z0-9]+)*$",
        RegexOptions.Compiled | RegexOptions.IgnoreCase);

    public bool Match(HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (values.TryGetValue(routeKey, out var value) && value != null)
        {
            var slug = value.ToString();
            return !string.IsNullOrEmpty(slug) && _regex.IsMatch(slug);
        }

        return false;
    }
}

// Register the constraint
var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add("slug", typeof(SlugConstraint));
});

var app = builder.Build();

// Use the custom constraint
app.MapGet("/blog/{slug:slug}", (string slug) =>
    $"Blog post: {slug}");

app.Run();

الاختبار:

GET /blog/my-first-post          → ✅ "Blog post: my-first-post"
GET /blog/hello-world-2024       → ✅ "Blog post: hello-world-2024"
GET /blog/My_Invalid_Slug        → ❌ 404
GET /blog/has spaces             → ❌ 404

الأولويات والأوامر

عند مباريات الطرق المتعددة، يستخدم نظام ASP.NET الأساسي نظاماً ذا أولوية:

graph TD
    A[Incoming Request] --> B{Route Matching}
    B --> C[Order by Priority]
    C --> D[ Literal segments highest]
    D --> E[ Constrained parameters]
    E --> F[ Unconstrained parameters]
    F --> G[ Optional parameters]
    G --> H[ Catch-all parameters lowest]
    H --> I[Select first match]


مثال:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Priority 1: Literal segments (highest priority)
app.MapGet("/users/admin", () => "Admin user endpoint");

// Priority 2: Constrained parameter
app.MapGet("/users/{id:int}", (int id) => $"User {id}");

// Priority 3: Unconstrained parameter
app.MapGet("/users/{username}", (string username) => $"User @{username}");

// Priority 4: Optional parameter
app.MapGet("/users/{id:int?}", (int? id) =>
    id.HasValue ? $"User {id}" : "All users");

// Priority 5: Catch-all (lowest priority)
app.MapGet("/users/{*path}", (string path) => $"Catch-all: {path}");

app.Run();

متطابقاً سلوك:

GET /users/admin        → "Admin user endpoint" (literal match)
GET /users/123          → "User 123" (constrained parameter)
GET /users/john         → "User @john" (unconstrained parameter)
GET /users/admin/test   → "Catch-all: admin/test" (catch-all)

يمكنك أيضاً أن تضع ترتيباً صريحاً:

app.MapGet("/products/{id}", (int id) => $"Product {id}")
    .WithOrder(1);

app.MapGet("/products/featured", () => "Featured products")
    .WithOrder(0); // Lower number = higher priority

لجنة

SPS.net الأساسية يمكن أن تربط بارامترات الطرق من مصادر متعددة:

app.MapGet("/users/{id}/posts/{postId}", (int id, int postId) =>
    $"User {id}, Post {postId}");

app.MapGet("/search", (string? q, int page = 1, int pageSize = 10) =>
    $"Query: {q}, Page: {page}, Size: {pageSize}");

// GET /search?q=aspnet&page=2&pageSize=20

app.MapGet("/api/data", ([FromHeader(Name = "X-API-Key")] string apiKey) =>
    $"API Key: {apiKey}");

هيئة

app.MapPost("/users", ([FromBody] User user) =>
{
    return Results.Created($"/users/{user.Id}", user);
});

public record User(int Id, string Name, string Email);

)الخدمــات )الخدمــات خــ خدمات

app.MapGet("/data", (IMyService service) =>
{
    var data = service.GetData();
    return data;
});

HtttpConCon Name

app.MapGet("/info", (HttpContext context) =>
{
    var userAgent = context.Request.Headers["User-Agent"];
    var ip = context.Connection.RemoteIpAddress;

    return new
    {
        UserAgent = userAgent.ToString(),
        IpAddress = ip?.ToString()
    };
});

لجنة

public record CreatePostRequest(string Title, string Content, List<string> Tags);

app.MapPost("/api/posts",
    async (
        [FromRoute] int userId,
        [FromQuery] bool publish,
        [FromBody] CreatePostRequest request,
        [FromHeader(Name = "X-Request-ID")] string requestId,
        [FromServices] IPostService postService,
        HttpContext context) =>
    {
        var post = await postService.CreatePostAsync(
            userId,
            request.Title,
            request.Content,
            request.Tags,
            publish
        );

        context.Response.Headers["X-Request-ID"] = requestId;

        return Results.Created($"/api/posts/{post.Id}", post);
    });

// POST /api/posts?publish=true
// Header: X-Request-ID: abc123
// Body: { "title": "Hello", "content": "World", "tags": ["aspnet", "routing"] }

نقطة النهاية

يمكن أن يكون لنقاط الوصل بيانات فوقية تصفها:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthorization();

// Add metadata with extension methods
app.MapGet("/public", () => "Public endpoint")
    .WithName("GetPublic")
    .WithDisplayName("Public Endpoint")
    .WithDescription("A public endpoint that anyone can access")
    .WithTags("Public");

app.MapGet("/private", () => "Private endpoint")
    .RequireAuthorization() // Adds [Authorize] metadata
    .WithName("GetPrivate");

app.MapGet("/admin", () => "Admin only")
    .RequireAuthorization("AdminPolicy");

// Access metadata
app.Map("/metadata", (IEndpointRouteBuilder endpoints) =>
{
    var dataSources = endpoints.DataSources;

    var endpointList = dataSources
        .SelectMany(ds => ds.Endpoints)
        .OfType<RouteEndpoint>()
        .Select(e => new
        {
            Name = e.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName,
            DisplayName = e.DisplayName,
            Route = e.RoutePattern.RawText,
            RequiresAuth = e.Metadata.GetMetadata<IAuthorizeData>() != null
        });

    return endpointList;
});

app.Run();

الناتج مـن /metadata:

[
  {
    "name": "GetPublic",
    "displayName": "Public Endpoint",
    "route": "/public",
    "requiresAuth": false
  },
  {
    "name": "GetPrivate",
    "displayName": null,
    "route": "/private",
    "requiresAuth": true
  },
  {
    "name": null,
    "displayName": null,
    "route": "/admin",
    "requiresAuth": true
  }
]

مجموعات

النقاط الفرعية ذات الصلة بالمجموعات والتشكيلات المتقاسمة:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Create a group with common prefix
var api = app.MapGroup("/api");

// All these routes are prefixed with /api
api.MapGet("/users", () => "Get all users");
api.MapGet("/users/{id}", (int id) => $"Get user {id}");
api.MapPost("/users", () => "Create user");

// Nested groups
var v1 = api.MapGroup("/v1");
v1.MapGet("/products", () => "V1 products");

var v2 = api.MapGroup("/v2");
v2.MapGet("/products", () => "V2 products");

// Groups with shared metadata
var adminApi = app.MapGroup("/admin")
    .RequireAuthorization("AdminPolicy")
    .WithTags("Admin");

adminApi.MapGet("/users", () => "Admin: Get users");
adminApi.MapDelete("/users/{id}", (int id) => $"Admin: Delete user {id}");

app.Run();

الطرق المُنشأة:

GET    /api/users
GET    /api/users/{id}
POST   /api/users
GET    /api/v1/products
GET    /api/v2/products
GET    /admin/users          [Authorize(AdminPolicy)]
DELETE /admin/users/{id}     [Authorize(AdminPolicy)]

المرئية:

flowchart TD
    Root[Root] --> Api[api]
    Root --> Admin[admin auth]

    Api --> Users1[GET users]
    Api --> Users2[GET users id]
    Api --> Users3[POST users]
    Api --> V1[v1]
    Api --> V2[v2]

    V1 --> V1Products[GET products]
    V2 --> V2Products[GET products]

    Admin --> AdminUsers[GET users]
    Admin --> AdminDelete[DELETE users id]

المستخلصات المتقدمة

المُحَكِكِكِكْرِرْمْر

app.MapGet("/data.json", () => Results.Json(new { data = "JSON" }));
app.MapGet("/data.xml", () => Results.Text("<data>XML</data>", "application/xml"));
app.MapGet("/data", (HttpContext context) =>
{
    var accept = context.Request.Headers["Accept"].ToString();

    if (accept.Contains("application/json"))
        return Results.Json(new { data = "JSON" });

    if (accept.Contains("application/xml"))
        return Results.Text("<data>XML</data>", "application/xml");

    return Results.Json(new { data = "Default JSON" });
});

الطرق المحلية

// English routes
app.MapGet("/en/products", () => "Products");
app.MapGet("/en/about", () => "About Us");

// Spanish routes
app.MapGet("/es/productos", () => "Productos");
app.MapGet("/es/acerca", () => "Acerca de Nosotros");

// Dynamic culture from route
app.MapGet("/{culture}/home", (string culture) =>
{
    CultureInfo.CurrentCulture = new CultureInfo(culture);
    return $"Home (Culture: {culture})";
});

الأمم المتحدة

// Version in route
var v1 = app.MapGroup("/api/v1");
v1.MapGet("/users", () => new { version = 1, users = new[] { "Alice", "Bob" } });

var v2 = app.MapGroup("/api/v2");
v2.MapGet("/users", () => new { version = 2, users = new[]
{
    new { id = 1, name = "Alice" },
    new { id = 2, name = "Bob" }
}});

// Version in query string
app.MapGet("/api/users", (int version = 1) =>
{
    if (version == 2)
        return Results.Json(new { version = 2, users = new[]
        {
            new { id = 1, name = "Alice" },
            new { id = 2, name = "Bob" }
        }});

    return Results.Json(new { version = 1, users = new[] { "Alice", "Bob" } });
});

// Version in header
app.MapGet("/api/data", (HttpContext context) =>
{
    var version = context.Request.Headers["X-API-Version"].FirstOrDefault() ?? "1";
~~~~
    return version switch
    {
        "2" => Results.Json(new { version = 2, data = "New format" }),
        _ => Results.Json(new { version = 1, data = "Old format" })
    };
});

app.Use(async (context, next) =>
{
    var host = context.Request.Host.Host;

    if (host.StartsWith("api."))
    {
        context.Request.RouteValues["area"] = "api";
    }
    else if (host.StartsWith("admin."))
    {
        context.Request.RouteValues["area"] = "admin";
    }

    await next(context);
});

app.MapGet("/", (HttpContext context) =>
{
    var area = context.Request.RouteValues["area"]?.ToString() ?? "main";

    return area switch
    {
        "api" => "API area",
        "admin" => "Admin area",
        _ => "Main area"
    };
});

& مُزززج

اعرض كلّ الدّور المسجّلة:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/users", () => "Users");
app.MapPost("/users", () => "Create user");
app.MapGet("/products/{id:int}", (int id) => $"Product {id}");

// Endpoint to inspect all routes
app.MapGet("/routes", (IEnumerable<EndpointDataSource> endpointSources) =>
{
    var endpoints = endpointSources
        .SelectMany(es => es.Endpoints)
        .OfType<RouteEndpoint>();

    var routes = endpoints.Select(e => new
    {
        Name = e.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName,
        Pattern = e.RoutePattern.RawText,
        Order = e.Order,
        HttpMethods = e.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods,
        Metadata = e.Metadata.Select(m => m.GetType().Name)
    });

    return Results.Json(routes);
});

app.Run();

الناتج مـن /routes:

[
  {
    "name": null,
    "pattern": "/users",
    "order": 0,
    "httpMethods": ["GET"],
    "metadata": ["HttpMethodMetadata", ...]
  },
  {
    "name": null,
    "pattern": "/users",
    "order": 0,
    "httpMethods": ["POST"],
    "metadata": ["HttpMethodMetadata", ...]
  },
  {
    "name": null,
    "pattern": "/products/{id:int}",
    "order": 0,
    "httpMethods": ["GET"],
    "metadata": ["HttpMethodMetadata", "RoutePatternMetadata", ...]
  }
]

لجنة البلدان

& كساق

// Routes are compiled and cached at startup
// This is fast:
for (int i = 0; i < 1000; i++)
{
    app.MapGet($"/endpoint{i}", () => $"Endpoint {i}");
}

// At runtime, route matching is O(1) for most cases

عيّن العناوين من الدروب:

app.MapGet("/users/{id}", (int id) => $"User {id}")
    .WithName("GetUser");

app.MapGet("/generate-link", (LinkGenerator linkGenerator, HttpContext context) =>
{
    var url = linkGenerator.GetPathByName("GetUser", new { id = 123 });
    // url = "/users/123"

    var absoluteUrl = linkGenerator.GetUriByName(context, "GetUser", new { id = 123 });
    // absoluteUrl = "https://localhost:5001/users/123"

    return new { url, absoluteUrl };
});

يُقتدِد على

  • التوجيه هو عملية من مرحلتين: المطابقة (المطابقة) والتنفيذ (النقاط النهاية)
  • وضع نماذج طرق لدعم البارامترات والمعوّقات والقيم الاختيارية والمصيد المُجمّل
  • (أ) القيود المتعلقة بالمقومات والأنماط
  • القيود الجمركية التي تتيح المصادقة على كل طلب على حدة
  • المسار: المواد الحرفية > البارامترات المقيدة > غير المقيدة < الاختيارية >
  • يمكن لبارامترات الربط من الطرق، سلسلة استعلام، ترويسة، جسم، وخدمات
  • البيانات الوصفية عند نقطة النهاية التي تمكِّن من سمات مثل الإذن والوثائق
  • أفرقة الطرق
  • أنشئ إنشاء إنشاء إنشاء إنشاء إنشاء العناوين من أسماء الطريق
  • نظام تحديد المسارات هو إلى درجة عالية من الاستخدام الأمثل للأداء

المسار و نقاط النهاية تشكل الجسر بين طلبات HTTP و رمز تطبيقك. تَأَمُّل هذا النظام يعطيك تحكماً دقيقاً على كيفية رسم URLs إلى الدالة.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.