Skip to main content
All docs
V23.2
.NET 6.0+

Execute CRUD Operations for Non-Persistent Objects

  • 6 minutes to read

This topic describes how to implement Create, Read, Update, and Delete operations for Non-Persistent Objects.

Note

This option of our Web API Service ships as part of the DevExpress Universal Subscription.

You may need Non-Persistent Objects if you develop a project with the help of the XAF. Use XAF documentation to familiarize yourself with the concept, if necessary. Note that the Web API Service requires you to implement certain steps in a different manner. The sections below highlight important differences.

Use the steps below as the implementation guide:

  • Declare a Non-Persistent Object
  • Create an Endpoint for the New Type
  • Enable the Object Space Provider for Non-Persistent Types
  • Create a Singleton Service for Data Access
  • Implement an Object Space Customizer Service

Declare a Non-Persistent Object

The following code implements a new CustomNonPersistentObject type:

  • The Name property stores string values.
  • The DomainComponentAttribute marks the object as non-persistent.
  • The base class - NonPersistentBaseObject - implements the key field property.
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.DC;

[DomainComponent]
public class CustomNonPersistentObject : NonPersistentBaseObject {
    public string Name { get; set; }
}

Create an Endpoint for the New Type

The code in this section creates an endpoint for CustomNonPersistentObject.

In Startup.cs, add or find the AddXafWebApi() method call and use the BusinessObject() method to create an endpoint.

public class Startup {
    public void ConfigureServices(IServiceCollection services) {
        // ...
        services.AddXafWebApi(builder => {
            builder.ConfigureOptions(options => {
                options.BusinessObject<CustomNonPersistentObject>();
            });
        }, Configuration);
    }
    // ...
}

You can review the following topic to learn about endpoint creation: Add CRUD Endpoints and Consume the Web API.

Enable the Non-Persistent Object Space Provider

The Solution Wizard automatically adds registration code for the Non-Persistent Object Space Provider. Make sure that the following code is present in your Web API Service project’s Startup.cs file. Add it if needed (in case you didn’t use the Solution Wizard).

File: MySolution.WebApi\Startup.cs (standalone), MySolution.Blazor.Server\Startup.cs (integrated)

builder.ObjectSpaceProviders
    //...
    .AddNonPersistent();

Create a Singleton Service for Data Access

The following class will enable data storage and access. The ObjectCache property - a Dictionary - holds the object list. The constructor initializes this list with a few objects.

Note

If you are familiar with Non-Persistent Object management in XAF applications, this step is new for you. You need to implement a storage in this manner because our Web API Service doesn’t work with the ValueManager.

using DevExpress.ExpressApp;

public class NonPersistentObjectStorageService {
    public Dictionary<Guid, NonPersistentBaseObject> 
      ObjectCache { get; } = new();

    public NonPersistentObjectStorageService() {
        CreateObject<CustomNonPersistentObject>("A");
        CreateObject<CustomNonPersistentObject>("B");
        CreateObject<CustomNonPersistentObject>("C");
    }
    private NonPersistentBaseObject CreateObject<T>(string value) where T : NonPersistentBaseObject, new() {
        T result = new T();
        if(result is CustomNonPersistentObject custom) {
            custom.Name = value;
        }
        ObjectCache.Add(result.Oid, result);
        return result;
    }
}

Register the class as a singleton service. Add the following line to Startup.cs:

public void ConfigureServices(IServiceCollection services) {
    services.AddSingleton<NonPersistentObjectStorageService>();
    //...
}

Tip

You can register this service as a scoped service. The lifetime of such a service depends on whether you host the Web API Service as part of a Blazor Server project or as a standalone ASP.NET Core project.

  • Integrated (Single Project - Blazor Server Application)
    The service’s visibility scope matches the lifetime of the Blazor app’s user circuit. The application typically releases a circuit if a user closes the browser tab or window.
  • Standalone (Separate Projects)
    The service’s visibility scope matches the lifetime of a single request.

The XAF Solution Wizard allows you to select the integration style on the Choose Options page.

Integrated or Standalone Web API Service Project

These controls appear if you selected both Web and Service on the Choose Target Platforms page.

Implement an Object Space Customizer Service

In this step, you implement an Object Space Customizer. This service holds a reference to the data storage you defined in the previous step. It can also access the NonPersistentObjectSpace to handle the following events:

  • ObjectGetting - obtains all objects from the storage.
  • ObjectByKeyGetting - obtains an object by its key value.
  • Committing - updates the storage in response to CREATE or DELETE operations.

Note

If you are familiar with Non-Persistent Object management in XAF applications, this step is new for you. XAF allows you to handle the Application.ObjectSpaceCreated event. You cannot access this event in Web API Service applications, so you need to implement an IObjectSpaceCustomizer instead.

using System.ComponentModel;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Core;

//Scoped service
public class NonPersistentObjectSpaceCustomizer : IObjectSpaceCustomizer {
    private readonly IObjectSpaceProviderService objectSpaceProvider;
    private readonly IObjectSpaceCustomizerService objectSpaceCustomizerService;

    private Dictionary<Guid, NonPersistentBaseObject> ObjectCache { get; }

    public NonPersistentObjectSpaceCustomizer(
      NonPersistentObjectStorageService nonPersistentStorage, 
      IObjectSpaceProviderService objectSpaceProvider, 
      IObjectSpaceCustomizerService objectSpaceCustomizerService) {
        ObjectCache = nonPersistentStorage.ObjectCache;
        this.objectSpaceProvider = objectSpaceProvider;
        this.objectSpaceCustomizerService = objectSpaceCustomizerService;
    }

    public void OnObjectSpaceCreated(IObjectSpace objectSpace) {
        if(objectSpace is NonPersistentObjectSpace nonPersistentObjectSpace) {
            nonPersistentObjectSpace.ObjectsGetting += NonPersistentObjectSpace_ObjectsGetting;
            nonPersistentObjectSpace.ObjectByKeyGetting += NonPersistentObjectSpace_ObjectByKeyGetting;
            nonPersistentObjectSpace.Committing += NonPersistentObjectSpace_Committing;
            nonPersistentObjectSpace.PopulateAdditionalObjectSpaces(objectSpaceProvider, objectSpaceCustomizerService);
        }
    }

    private void NonPersistentObjectSpace_ObjectsGetting(object? sender, ObjectsGettingEventArgs e) {
        if(typeof(NonPersistentBaseObject).IsAssignableFrom(e.ObjectType)) {
            ArgumentNullException.ThrowIfNull(sender);
            IObjectSpace objectSpace = (IObjectSpace)sender;
            var objects = new BindingList<NonPersistentBaseObject>() {
                AllowNew = true,
                AllowEdit = true,
                AllowRemove = true
            };
            foreach(var obj in ObjectCache.Values) {
                if(e.ObjectType.IsAssignableFrom(obj.GetType()))
                    objects.Add(objectSpace.GetObject(obj));
            }
            e.Objects = objects;
        }
    }
    private void NonPersistentObjectSpace_ObjectByKeyGetting(object? sender, ObjectByKeyGettingEventArgs e) {
        ArgumentNullException.ThrowIfNull(sender);
        IObjectSpace objectSpace = (IObjectSpace)sender;
        if(typeof(NonPersistentBaseObject).IsAssignableFrom(e.ObjectType)) {
            NonPersistentBaseObject? obj;
            if(ObjectCache.TryGetValue((Guid)e.Key, out obj)) {
                e.Object = objectSpace.GetObject(obj);
            }
        }
    }
    private void NonPersistentObjectSpace_Committing(object? sender, CancelEventArgs e) {
        ArgumentNullException.ThrowIfNull(sender);
        IObjectSpace objectSpace = (IObjectSpace)sender;
        foreach(var obj in objectSpace.ModifiedObjects) {
            if(obj is NonPersistentBaseObject nonPersistentObject) {
                if(objectSpace.IsNewObject(obj)) {
                    ObjectCache.Add(nonPersistentObject.Oid, nonPersistentObject);
                }
                else if(objectSpace.IsDeletedObject(obj)) {
                    ObjectCache.Remove(nonPersistentObject.Oid);
                }
            }
        }
    }
}

Register the service. Add the following TryAddEnumerable extension method call to Startup.cs.

public void ConfigureServices(IServiceCollection services) {
    //...
    services.TryAddEnumerable(ServiceDescriptor.Scoped<IObjectSpaceCustomizer, 
      NonPersistentObjectSpaceCustomizer>());
    //...
}

Unit Tests

Follow the steps below to add unit tests for the code that queries Non-Persistent Objects via the Web API Service.

  1. If your solution does not have a testing project, add a new xUnit test project.
  2. Add a reference to the {SolutionName}.Blazor.Server project.
  3. Add the Microsoft.AspNetCore.Mvc.Testing package reference.
  4. Add the following test to the test project:
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;

public class CRUDNonPersistentTests : IClassFixture<WebApplicationFactory<MySolution.Blazor.Server.Startup>> {
    readonly string url = $"/api/odata/{typeof(CustomNonPersistentObject).Name}";

    HttpClient httpClient;
    public CRUDNonPersistentTests(WebApplicationFactory<Blazor.Server.Startup> webApplicationFactory) {
        httpClient = webApplicationFactory.CreateClient();
    }

    [Fact]
    public async System.Threading.Tasks.Task Create_Read_Delete() {
        string tokenString = await GetUserTokenAsync("Sam", "", "/api/Authentication/Authenticate");
        var authorizationToken = new AuthenticationHeaderValue("Bearer", tokenString);

        string content = $"{{\"{nameof(CustomNonPersistentObject.Name)}\":'Test Data'}}";
        var httpRequest = new HttpRequestMessage(HttpMethod.Post, url);
        httpRequest.Content = new StringContent(content, Encoding.UTF8, "application/json");
        httpRequest.Headers.Authorization = authorizationToken;
        var responce = await httpClient.SendAsync(httpRequest);
        Assert.Equal(HttpStatusCode.Created, responce.StatusCode);

        var jsonResult = await responce.Content.ReadFromJsonAsync<JsonElement>();
        var newNonPersistentObject = jsonResult.Deserialize(typeof(CustomNonPersistentObject)) as CustomNonPersistentObject;
        ArgumentNullException.ThrowIfNull(newNonPersistentObject);
        var objKey = newNonPersistentObject.Oid;

        try {
            Assert.Equal("Test Data", newNonPersistentObject.Name);

            //Get by key
            var getHttpRequest = new HttpRequestMessage(HttpMethod.Get, $"{url}/{objKey}");
            getHttpRequest.Headers.Authorization = authorizationToken;
            var getResponce = await httpClient.SendAsync(getHttpRequest);
            Assert.Equal(HttpStatusCode.OK, getResponce.StatusCode);

            jsonResult = await getResponce.Content.ReadFromJsonAsync<JsonElement>();
            var loadedObj = jsonResult.Deserialize(typeof(CustomNonPersistentObject)) as CustomNonPersistentObject;
            ArgumentNullException.ThrowIfNull(loadedObj);
            Assert.Equal("Test Data", loadedObj.Name);
        }
        finally {
            //Delete the test object
            var deleteHttpRequest = new HttpRequestMessage(HttpMethod.Delete, $"{url}/{objKey}");
            deleteHttpRequest.Headers.Authorization = authorizationToken;
            var deleteResponce = await httpClient.SendAsync(deleteHttpRequest);
            Assert.Equal(HttpStatusCode.OK, deleteResponce.StatusCode);
        }
    }

    async Task<string> GetUserTokenAsync(string userName, string password, string requestPath) {
        var request = new HttpRequestMessage(HttpMethod.Post, requestPath);
        request.Content = new StringContent(
            $"{{ \"userName\": \"{userName}\", \"password\": \"{password}\" }}", Encoding.UTF8, "application/json");

        var httpResponse = await httpClient.SendAsync(request);
        if(!httpResponse.IsSuccessStatusCode) {
            throw new UnauthorizedAccessException($"Authorization request failed! Code {(int)httpResponse.StatusCode}, '{httpResponse.ReasonPhrase}'");
        }
        var tokenString = await httpResponse.Content.ReadAsStringAsync();
        return tokenString;
    }
}