DEV Community

Theodoros Bebekis
Theodoros Bebekis

Posted on

Fundamental Concepts of Business Applications IV: Building a Locator

From declaration to runtime

In the previous parts we examined the problem: a business application needs a reliable way to turn a human reference into a business entity.

A user types a customer code, a product name, a tax number, an external identifier, or a fragment of a description. The application needs to arrive at something stable: the canonical identity of the record.

That is the role of the Locator.

In the previous article we saw how a Locator can be described declaratively. We described fields, search fields, result fields, key field, and policies. But a definition by itself is just data. It needs a runtime.

In this article we move to the practical part:

Locator Definition
        ↓
Runtime Request
        ↓
Execution Engine
        ↓
Locator Result
Enter fullscreen mode Exit fullscreen mode

We will see which objects are needed, what their roles are, how they cooperate, and how a declarative definition becomes an executable mechanism.

The article uses the Locator as a case study, but the idea is broader. Many business frameworks follow the same path: we describe something declaratively and then build a runtime that can execute it.

1. The problem solved by the Locator

In a sales application, the user may type:

ALFKI
Enter fullscreen mode Exit fullscreen mode

or:

Alfreds
Enter fullscreen mode Exit fullscreen mode

or select a row from a result list.

For the UI, these look like text input or a dropdown selection. For the business layer, they are something else: an attempt to resolve a reference.

The application wants to answer:

  • Which record does this refer to?
  • Is there exactly one record?
  • Are there multiple records and should the user choose one?
  • Is the search too broad?
  • Is the request invalid?
  • Is there a context problem, such as wrong company or permissions?

If this logic stays inside event handlers, SQL snippets, or controllers, the system fills with repetition and special cases. The Locator provides a stable runtime contract.

2. The basic runtime pieces

A Locator needs five basic objects:

LocatorDefinition
LocatorRequest
LocatorContext
LocatorEngine
LocatorResult
Enter fullscreen mode Exit fullscreen mode

Their roles are clear:

  • LocatorDefinition: describes what the locator knows.
  • LocatorRequest: describes what the caller is asking for now.
  • LocatorContext: carries runtime information.
  • LocatorEngine: executes the process.
  • LocatorResult: returns outcome and data.

These objects are the minimum runtime model. They are more than classes for organizing code: they are the boundaries of the mechanism.

3. LocatorDefinition

The definition is the declarative part.

A simple shape:

public class LocatorDefinition
{
    public string Name { get; set; }
    public string ClassName { get; set; }
    public string Source { get; set; }
    public string KeyField { get; set; }

    public List<LocatorFieldDefinition> Fields { get; } = new();
    public List<string> SearchFields { get; } = new();
    public List<string> ResultFields { get; } = new();

    public int MinimumSearchLength { get; set; }
    public int MaximumResultCount { get; set; }
    public string OrderBy { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Example:

var customerLocator = new LocatorDefinition
{
    Name = "Customer",
    ClassName = "SqlLocator",
    Source = "Customer",
    KeyField = "Id",
    OrderBy = "Name",
    MinimumSearchLength = 2,
    MaximumResultCount = 50
};

customerLocator.AddField("Id", FieldType.String);
customerLocator.AddField("Code", FieldType.String);
customerLocator.AddField("Name", FieldType.String);

customerLocator.AddSearchField("Code");
customerLocator.AddSearchField("Name");

customerLocator.AddResultField("Id");
customerLocator.AddResultField("Code");
customerLocator.AddResultField("Name");
Enter fullscreen mode Exit fullscreen mode

This code describes the Locator. Search, connection, UI, and result state belong to the runtime.

The definition answers:

  • What is the locator name?
  • Which engine will execute it?
  • Which source will the engine use?
  • Which field is the canonical identity?
  • Which fields participate in search?
  • Which fields are returned?
  • Which policies apply?

The definition stays stable. The request and context change on every execution.

4. Name, ClassName, and Source

The three fields Name, ClassName, and Source are easy to confuse, so it is worth separating them.

Name
    locator identity in the registry

ClassName
    engine/handler type that will execute the definition

Source
    provider-specific source used by the engine
Enter fullscreen mode Exit fullscreen mode

Name is the registry key:

Customer
Enter fullscreen mode Exit fullscreen mode

The runtime uses it to find the definition.

ClassName selects the engine:

SqlLocator
ServiceLocator
ObjectLocator
Enter fullscreen mode Exit fullscreen mode

Source is input to the engine. For a SQL engine it may be a table:

Customer
Enter fullscreen mode Exit fullscreen mode

or a select:

select Id, Code, Name
from Customer
where IsActive = 1
Enter fullscreen mode Exit fullscreen mode

For a service engine it may be an endpoint:

/api/customers/search
Enter fullscreen mode Exit fullscreen mode

For an object engine it may be the name of an in-memory collection.

The architecture remains provider-neutral. Interpreting Source belongs to the engine.

5. LocatorRequest

The request is the runtime intention of the caller.

A simple shape:

public class LocatorRequest
{
    public object KeyValue { get; set; }
    public string SearchTerm { get; set; }
    public string SearchField { get; set; }
    public bool IsMultiRow { get; set; }

    public LocatorContext Context { get; set; } = new();
}
Enter fullscreen mode Exit fullscreen mode

KeyValue is used when the caller already has a possible canonical identity and wants to load or verify it.

SearchTerm is used for text search.

SearchField optionally limits the search to a specific field.

IsMultiRow gives a hint about the UI scenario, for example a grid lookup instead of a simple editor.

The request expresses the caller's intention. Fields, policies, and source belong to the definition.

The request says:

I want to resolve this now.
Enter fullscreen mode Exit fullscreen mode

6. LocatorContext

The context carries runtime conditions.

The same search may produce different results per company, branch, tenant, language, connection, date, or user permissions.

A practical shape:

public class LocatorContext
{
    public string LocatorName { get; set; }

    public Dictionary<string, object> Parameters { get; } = new();
}
Enter fullscreen mode Exit fullscreen mode

Example:

request.Context.LocatorName = "Customer";
request.Context.Parameters["CompanyId"] = currentCompanyId;
request.Context.Parameters["Language"] = currentLanguage;
request.Context.Parameters["ConnectionName"] = connectionName;
Enter fullscreen mode Exit fullscreen mode

The dictionary is a practical choice. The context works as a runtime carrier, not as an entity schema. It needs room for parameters that vary per application or provider.

In stricter implementations there may be a strongly typed context. The basic contract remains the same: the context brings information that affects execution.

LocatorName in the context is registry identity. The runtime uses it to find the definition.

7. LocatorResult

The result is an outcome, not just a list.

A search function usually returns rows. A Locator must return the state of the resolution attempt.

A simple shape:

public class LocatorResult
{
    public LocatorResultStatus Status { get; set; }
    public string Message { get; set; }

    public object Data { get; set; }

    public int Count { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

The basic states:

public enum LocatorResultStatus
{
    None,
    InvalidRequest,
    InvalidContext,
    NoResult,
    SingleResult,
    MultipleResults,
    TooManyResults,
    Error
}
Enter fullscreen mode Exit fullscreen mode

The distinction has practical value:

  • NoResult: the request was valid, but the search returned an empty result.
  • SingleResult: one record was found and the reference was resolved.
  • MultipleResults: a small set of records was found and the user can choose.
  • TooManyResults: the search was too broad.
  • InvalidRequest: input is missing or invalid.
  • InvalidContext: the runtime context is incomplete or incompatible.
  • Error: a technical or unexpected failure.

The UI can react differently to each state.

For example:

SingleResult
    write CustomerId and related fields

MultipleResults
    show selection list

TooManyResults
    ask for more characters

NoResult
    show message or allow new record
Enter fullscreen mode Exit fullscreen mode

This is where the Locator becomes a business mechanism instead of a simple search helper.

8. LocatorEngine

The engine executes.

A clean contract:

public abstract class Locator
{
    public abstract LocatorResult Execute(
        LocatorDefinition definition,
        LocatorRequest request);
}
Enter fullscreen mode Exit fullscreen mode

The engine receives a definition and a request. The context is inside the request.

Its role:

Validate Request
Read Definition
Apply Context
Resolve
Produce Canonical Identity
Produce Projection
Determine Result State
Return Result
Enter fullscreen mode Exit fullscreen mode

The implementation may be SQL, service, object, cache, directory, or something else.

The contract remains the same.

9. Facade, Registry, and Factory

The caller needs a simple entry point.

The facade takes responsibility for:

find definition
validate definition
create engine
execute engine
interpret result
Enter fullscreen mode Exit fullscreen mode

It needs an application-level service.

Example:

public static class Locators
{
    public static LocatorResult Execute(LocatorRequest request)
    {
        if (request == null)
            throw new ArgumentNullException(nameof(request));

        if (request.Context == null)
            return LocatorResult.InvalidContext("Missing locator context.");

        if (string.IsNullOrWhiteSpace(request.Context.LocatorName))
            return LocatorResult.InvalidContext("Missing locator name.");

        LocatorDefinition definition =
            LocatorRegistry.Get(request.Context.LocatorName);

        definition.Check();

        Locator locator =
            LocatorFactory.Create(definition.ClassName);

        return locator.Execute(definition, request);
    }
}
Enter fullscreen mode Exit fullscreen mode

The roles:

  • Registry: finds a definition by locator name.
  • Factory: creates an engine based on ClassName.
  • Facade: gives the caller a simple entry point.
  • Engine: executes resolution.

This keeps UI and callers clean.

10. The execution pipeline

Now let us look at the pipeline as practical steps.

Caller
  ↓
LocatorRequest
  ↓
LocatorContext.LocatorName
  ↓
LocatorRegistry
  ↓
LocatorDefinition
  ↓
LocatorFactory
  ↓
LocatorEngine
  ↓
LocatorResult
Enter fullscreen mode Exit fullscreen mode

Validate Request

The engine checks input first.

Example:

if (request.KeyValue == null &&
    !string.IsNullOrWhiteSpace(request.SearchTerm) &&
    request.SearchTerm.Length < definition.MinimumSearchLength)
{
    return new LocatorResult
    {
        Status = LocatorResultStatus.InvalidRequest,
        Message = "The search term is too short."
    };
}
Enter fullscreen mode Exit fullscreen mode

The limit lives in the definition. The check happens at runtime.

Read Definition

The engine reads:

Source
KeyField
SearchFields
ResultFields
Policies
Enter fullscreen mode Exit fullscreen mode

The definition provides materials and rules. The engine decides how to use them.

Apply Context

The context may select connection, company, tenant, language, or permissions.

The same definition can execute differently per context.

Resolve

This is the provider-specific part.

In a SQL engine, a query is produced.

In a service engine, a service call is made.

In an object engine, a collection is searched.

The result of this step is raw resolved data.

Produce Canonical Identity

The Locator targets canonical identity.

KeyField declares which field plays that role.

KeyField = Id
Enter fullscreen mode Exit fullscreen mode

The user may have typed a code or name. The system ultimately wants Id.

Produce Projection

Often the caller needs additional fields:

Id
Code
Name
TaxNumber
PaymentTermId
Enter fullscreen mode Exit fullscreen mode

ResultFields define that projection.

The projection belongs to the result.

Applying those fields to a target row or target object is the job of another component, usually a mapper.

Determine Result State

The engine decides the status.

Example:

if (count == 0)
    status = LocatorResultStatus.NoResult;
else if (count == 1)
    status = LocatorResultStatus.SingleResult;
else if (count > definition.MaximumResultCount)
    status = LocatorResultStatus.TooManyResults;
else
    status = LocatorResultStatus.MultipleResults;
Enter fullscreen mode Exit fullscreen mode

The result state drives the UI and the workflow.

Return Result

The result returns:

Status
Message
Data
Count
Enter fullscreen mode Exit fullscreen mode

Data may be a table, DTO list, object list, or another container. That is an implementation concern.

11. SQL-based engine

In many business applications the first engine will be SQL-based.

That is practical, while the architecture remains provider-neutral.

For a SQL engine, Source may be a table:

Customer
Enter fullscreen mode Exit fullscreen mode

or a select:

select Id, Code, Name
from Customer
where IsActive = 1
Enter fullscreen mode Exit fullscreen mode

A SELECT as source is a convenience of the SQL implementation. It fits because SQL is a declarative language. It describes a result, not imperative steps.

Other engines will interpret Source differently.

The shared contract stays stable.

12. Declarative policies

Policies belong to the definition:

MinimumSearchLength
MaximumResultCount
OrderBy
Enter fullscreen mode Exit fullscreen mode

The engine applies them.

MinimumSearchLength protects against overly short searches.

MaximumResultCount protects the UI and backend from oversized result sets.

OrderBy determines presentation.

The definition declares. The engine executes.

That is the essence of the runtime model.

13. Resolution states in practice

The states deserve special attention because they directly affect the UI.

NoResult

The request executed and the search returned an empty result.

The UI may show a message, clear the field, or suggest creating a new record.

SingleResult

The reference was resolved cleanly.

The caller receives canonical identity and result fields.

MultipleResults

There are multiple records, but the count is manageable.

The UI shows choices.

Example:

SearchTerm = John
Result count = 5
Enter fullscreen mode Exit fullscreen mode

TooManyResults

There are many records and the search is too broad.

Example:

SearchTerm = s
Result count = 500
MaximumResultCount = 50
Enter fullscreen mode Exit fullscreen mode

The UI asks for more specific input.

The distinction from MultipleResults is critical:

MultipleResults
    show choices

TooManyResults
    ask for narrower search
Enter fullscreen mode Exit fullscreen mode

InvalidRequest

The input is invalid.

Examples:

Search term too short
Unknown search field
Missing required input
Invalid key value type
Enter fullscreen mode Exit fullscreen mode

InvalidContext

The context is missing, incomplete, or incompatible with execution.

Examples:

Missing company
Missing tenant
Invalid connection
Insufficient permissions
Unsupported language context
Enter fullscreen mode Exit fullscreen mode

Error

A technical or unexpected failure.

Examples:

Database error
Service unavailable
Timeout
Serialization error
Unexpected exception
Enter fullscreen mode Exit fullscreen mode

Business states and technical errors should stay separate.

14. Extensibility

Extensibility comes from the boundaries.

The definition is declarative.

The request is runtime input.

The context is runtime environment.

The result is outcome.

The engine is interpreter.

With these boundaries, we can have many engines:

SqlLocator
ServiceLocator
ObjectLocator
CacheLocator
DirectoryLocator
GraphLocator
Enter fullscreen mode Exit fullscreen mode

All of them implement the same basic contract:

public abstract class Locator
{
    public abstract LocatorResult Execute(
        LocatorDefinition definition,
        LocatorRequest request);
}
Enter fullscreen mode Exit fullscreen mode

The application continues to call Locators.Execute(request).

The engine change happens behind the definition.

This is practical extensibility. The caller stays free of if/else logic for every provider.

15. Mapper: a neighboring component

When the user selects a record, more than one field is often updated.

Example:

CustomerId
CustomerCode
CustomerName
PaymentTermId
VatRegimeId
Enter fullscreen mode Exit fullscreen mode

The Locator answers:

Which record corresponds to this reference?
Enter fullscreen mode Exit fullscreen mode

The Mapper answers:

How are the result fields written to the consumer?
Enter fullscreen mode Exit fullscreen mode

The Mapper consumes LocatorResult. It is a neighboring mechanism, outside the Locator core.

It deserves a separate article because it has its own concerns: field mapping, target object, target row, validation, overwrite policy, and transactional behavior.

16. Result materialization

The Data of the result is an implementation detail.

In a SQL engine it may be a table.

In a service engine it may be a DTO list.

In an object engine it may be an object list.

The contract asks for:

Status
Message
Data
Count
Enter fullscreen mode Exit fullscreen mode

The container is selected by the implementation.

This keeps the pattern clean.

17. Conclusion

The Locator starts from a practical business problem: how a user reference becomes a stable business identity.

The solution needs more than a search function.

It needs a runtime contract:

LocatorDefinition
        ↓
LocatorRequest
        ↓
LocatorContext
        ↓
LocatorEngine
        ↓
LocatorResult
Enter fullscreen mode Exit fullscreen mode

The definition declares.

The request asks.

The context carries runtime conditions.

The engine interprets and executes.

The result returns outcome and data.

This model gives clear boundaries:

  • The UI sends a request and reads a result.
  • The registry finds the definition.
  • The factory creates an engine.
  • The engine applies policies and provider-specific logic.
  • The result says what happened.

The same grammar appears in many parts of a business application framework:

Problem
        ↓
Architectural Concept
        ↓
Declarative Definition
        ↓
Executable Runtime Model
        ↓
Concrete Implementation
Enter fullscreen mode Exit fullscreen mode

The Locator is a good example because it is small, useful, and complex enough to show the pattern clearly.

A Locator is only one example. The important idea is the transformation itself: a declarative description becomes an executable runtime model. Once that pattern is established, the same architecture can be applied to forms, reports, workflows, validation rules, document handlers, and many other business components. The Locator simply provides a compact case study of a much broader architectural principle.

In the end, the goal is something larger than another helper class: a mechanism that can be reused, extended, and integrated into different UIs, data sources, and business workflows.

Top comments (0)