xaf-web-api

XAF Backend Web API Service - OData v4 REST endpoints, Program.cs setup with JWT auth, business object exposure with [AllowedAction]/[IgnoreDataMember], OData query options ($filter/$expand/$select/$orderby/$top/$skip/$count), JWT authentication flow, custom OData actions/functions via [Action] attribute, Swagger integration, security integration, file upload, v24.2 vs v25.1 differences. Use when building or consuming the XAF Web API backend service.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "xaf-web-api" with this command: npx skills add kashiash/xaf-skills/kashiash-xaf-skills-xaf-web-api

XAF: Backend Web API Service

Overview

XAF Web API Service provides OData v4 REST endpoints for XAF business objects. It can run standalone (no UI) or alongside a Blazor app. All XAF security, validation, and ObjectSpace features work the same way.


Project Setup

NuGet Packages

<PackageReference Include="DevExpress.ExpressApp.WebApi" Version="25.1.*" />
<PackageReference Include="DevExpress.ExpressApp.WebApi.Jwt" Version="25.1.*" />
<PackageReference Include="DevExpress.ExpressApp.WebApi.Swashbuckle" Version="25.1.*" />

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddXaf(builder.Configuration, b => {
    b.UseApplication<MyWebApiApplication>();

    b.AddObjectSpaceProviders(providers => {
        providers.UseEntityFramework(ef => {
            ef.DefaultDatabaseConnection("Default", p =>
                p.UseDbContext<MyDbContext>());
        });
        providers.AddNonPersistent();
    });

    b.Security
        .UseIntegratedMode(options => {
            options.RoleType = typeof(PermissionPolicyRole);
            options.UserType = typeof(ApplicationUser);
        })
        .AddPasswordAuthentication();

    b.AddWebApi(options => {
        options.BusinessObject<Contact>();      // expose specific type
        options.BusinessObject<Order>();
        // or expose all: options.BusinessObjects.AddFromAssembly(typeof(Contact).Assembly);
    });
});

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => {
        options.TokenValidationParameters = new TokenValidationParameters {
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });

builder.Services.AddSwaggerGen();

var app = builder.Build();
app.UseXaf();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.UseSwagger();
app.UseSwaggerUI();
app.Run();

Controlling Business Object Exposure

// Allow/deny specific CRUD operations
[AllowedAction(Operations.Read)]            // read-only
[AllowedAction(Operations.Create | Operations.Read | Operations.Write)]
public class Contact : BaseObject { ... }

// Exclude property from API response
[IgnoreDataMember]
public virtual string InternalNotes { get; set; }

// Programmatic exposure in Program.cs:
b.AddWebApi(options => {
    options.BusinessObject<Order>()
           .AllowedActions(Operations.Read | Operations.Create);
});

OData Queries

GET collection

GET /api/odata/Contact
GET /api/odata/Contact?$filter=City eq 'Warsaw'
GET /api/odata/Contact?$select=FirstName,LastName,Email
GET /api/odata/Contact?$expand=Department
GET /api/odata/Contact?$orderby=LastName asc,FirstName asc
GET /api/odata/Contact?$top=10&$skip=20&$count=true

GET single object

GET /api/odata/Contact(guid'00000000-0000-0000-0000-000000000001')

POST (create)

POST /api/odata/Contact
Content-Type: application/json
Authorization: Bearer <token>

{ "FirstName": "Jan", "LastName": "Kowalski", "Email": "jan@example.com" }

PATCH (update)

PATCH /api/odata/Contact(guid'...')
Content-Type: application/json
Authorization: Bearer <token>

{ "Email": "new@example.com" }

DELETE

DELETE /api/odata/Contact(guid'...')
Authorization: Bearer <token>

OData Filter Operators

OperatorExample
Equals$filter=Status eq 'Active'
Not equals$filter=Status ne 'Deleted'
Greater/Less$filter=Amount gt 100
Contains$filter=contains(Name, 'Smith')
StartsWith$filter=startswith(Name, 'Jan')
Null check$filter=Department eq null
AND/OR$filter=City eq 'Warsaw' and Active eq true
Date$filter=CreatedDate gt 2024-01-01

Authentication

Get Token

POST /api/Authentication/Authenticate
Content-Type: application/json

{ "userName": "Admin", "password": "" }

Response: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Use Token

GET /api/odata/Contact
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

HttpClient Example (C#)

var httpClient = new HttpClient { BaseAddress = new Uri("https://myapp.com") };

// Get token
var tokenResponse = await httpClient.PostAsJsonAsync(
    "/api/Authentication/Authenticate",
    new { userName = "Admin", password = "" });
var token = await tokenResponse.Content.ReadAsStringAsync();

// Use token
httpClient.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", token.Trim('"'));

// Query data
var contacts = await httpClient.GetFromJsonAsync<ODataResult<Contact>>(
    "/api/odata/Contact?$filter=Active eq true");

Custom Endpoints

Custom OData Action (POST, modifies data)

// In a controller (or XAF business logic class):
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase {
    private readonly IObjectSpaceFactory _objectSpaceFactory;

    public OrderController(IObjectSpaceFactory objectSpaceFactory) {
        _objectSpaceFactory = objectSpaceFactory;
    }

    [HttpPost("api/odata/Order({key})/ProcessOrder")]
    public IActionResult ProcessOrder([FromRoute] Guid key) {
        using var os = _objectSpaceFactory.CreateObjectSpace(typeof(Order));
        var order = os.GetObjectByKey<Order>(key);
        if (order == null) return NotFound();
        order.Status = OrderStatus.Processed;
        os.CommitChanges();
        return Ok();
    }
}

Or via XAF's [Action] attribute on a non-persistent class:

[DomainComponent]
public class OrderActions {
    [Action(Caption = "ProcessOrder")]
    public void ProcessOrder(Order order, IObjectSpace objectSpace) {
        order.Status = OrderStatus.Processed;
        objectSpace.CommitChanges();
    }
}

Swagger / OpenAPI

Automatically available at /swagger when configured. All OData endpoints are documented.


Security Integration

All OData endpoints automatically apply XAF security:

  • Permissions filter which records are returned (object-level security)
  • Member-level permissions hide specific properties
  • DenyAllByDefault → explicit grants required for API access

No additional configuration needed — security works transparently via ObjectSpace.


Pagination Pattern

GET /api/odata/Contact?$top=10&$skip=0&$count=true

Response includes @odata.count with total count. Client paginates by incrementing $skip.


v24.2 vs v25.1 Differences

Featurev24.2v25.1
OData versionv4v4 (same)
JWT authAvailableAvailable (enhanced config)
SwaggerAvailableAvailable
Batch requestsLimitedImproved
.NET target.NET 8.NET 8 / .NET 9

Source Links

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Security

xaf-security

No summary provided by upstream source.

Repository SourceNeeds Review
General

xaf-office

No summary provided by upstream source.

Repository SourceNeeds Review
General

xaf-winforms-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

xaf-deployment

No summary provided by upstream source.

Repository SourceNeeds Review