Introduction

Regardless of the platform, developers of enterprise apps encounter several challenges:

– Evolving app requirements over time.

– Emerging business opportunities and challenges.

– Continuous feedback during development that can significantly impact the app’s scope and requirements.

Building adaptable apps requires an architecture that allows for easy modification or extension over time. This involves designing the app so that individual parts can be developed and tested independently without affecting the rest of the app.

Many enterprise apps are complex and require multiple developers, making it challenging to design the app in a way that allows for independent development while ensuring seamless integration.

The traditional monolithic approach, where components are tightly coupled, often results in apps that are hard to maintain. Bugs are difficult to fix without impacting other components, and adding or replacing features is inefficient.

This article will help you understand and focus on the core patterns and architecture for building a cross-platform enterprise app using .NET MAUI. My article is designed to help you produce adaptable, maintainable, and testable code by addressing common .NET MAUI enterprise app development scenarios.

Model-View-ViewModel pattern (MVVM)

The MVVM (Model-View-ViewModel) pattern consists of three essential components: the model, the view, and the view model. Each element plays a distinct role in the architecture. The accompanying diagram visually depicts the intricate connections between these components.
Model-View-ViewModel pattern (MVVM)

The view defines the structural layout and visual presentation that users see on the screen. It’s recommended that each view be outlined in XAML, keeping the code minimal and free from business logic.

The view model defines properties and commands for data binding with the view. It notifies the view of any state changes through change notification events. These properties and commands outline the UI’s functionalities, while the view determines how they are displayed.

Model classes act as non-visual entities that encapsulate an application’s data. Essentially, the model represents the domain model of the application, often incorporating a data model alongside business and validation logic.

How to connect view models to views:

Create a view model declaratively:

– The simplest approach is for the view to declaratively create its associated view model in XAML. When the view is instantiated, so is the corresponding view model object. This method is demonstrated in the following code snippet.

Unset

<ContentPage xmlns:local=”clr-namespace:example”>
<ContentPage.BindingContext>
<local:LoginViewModel />
</ContentPage.BindingContext>
<!– Omitted for brevity… –>
</ContentPage>

Creating a view model programmatically

– In the code-behind file of a view, you can assign the view model to its BindingContext property. Typically, this assignment takes place in the view’s constructor, as shown in the following code example.

Unset

public LoginView()
{
InitializeComponent();
BindingContext = new LoginViewModel(navigationService);
}

Updating views in response to changes in the associated view model or model.

– Every view model and model class accessible to a view should implement the INotifyPropertyChanged interface. This interface allows the class to inform data-bound controls given any changes in the underlying property values.

– For example, the following code shows a simple ViewModel with properties that raise changes.

Unset

public class LoginViewModel : INotifyPropertyChanged
{
private string _email;
private string _password;

public event PropertyChangedEventHandler PropertyChanged;

public string Email
{
get => _email;
set => SetPropertyValue(ref _email, value);
}

public string Password
{
get => _password;
set => SetPropertyValue(ref _password, value);
}

protected void SetPropertyValue(ref T storageField, T newValue, [CallerMemberName] string propertyName = “”)
{
if (Equals(storageField, newValue))
return;

storageField = newValue;
RaisePropertyChanged(propertyName);
}

protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = “”)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

Using the MVVM toolkit in modular mobile applications enhances the architecture, making the app more maintainable, testable, and scalable.

– The MVVM pattern is well-established in .NET, supported by a variety of frameworks developed by the community to simplify its implementation. Each framework offers a unique set of features, but they generally include a standard view model with an implementation of the INotifyPropertyChanged interface. Additional functionalities often provided by MVVM frameworks include custom commands, navigation aids, dependency injection/service location modules, and seamless integration with UI platforms.

– The example below shows the same ViewModel using components that come with the MVVM Toolkit:

Unset

public partial class LoginViewModel : ObservableObject
{
[ObservableProperty]
private string _name;

[ObservableProperty]
private string _value;
}

ObservableObject manages all the logic required for raising change notifications by utilizing the SetProperty method within your property setter.

UI interaction through commands:

– In multi-platform applications, user interactions such as button clicks typically trigger actions, often implemented through event handlers in the code-behind file. However, within the MVVM pattern, executing these actions is the responsibility of the view model, and it is recommended to avoid placing code directly in the code behind.

– View models typically expose public properties that implement the ICommand interface for binding from the view.

– The Command or Command constructor requires an Action callback, which is called when the ICommand.Execute method is invoked. An optional constructor parameter, CanExecute, is a Func that returns a bool.

Unset

public class LoginViewModel : INotifyPropertyChanged
{
···
public ICommand LoginCommand;
public LoginViewModel()
{
LoginCommand = new Command(
execute: () =>
{
// verify user’s credential
isProcessing = true
RefreshCanExecutes();
},
canExecute: () =>
{
return !isProcessing;
});
···
}

void RefreshCanExecutes()
{
(LoginCommand as Command).ChangeCanExecute();
}
···
}

The MVVM Toolkit includes two commands: RelayCommand and AsyncRelayCommand. RelayCommand is suitable for executing synchronous code and shares a similar implementation with the .NET MAUI Command object.

AsyncRelayCommand offers numerous additional capabilities for handling asynchronous workflows. This is frequently encountered in our ViewModel, as it often involves communication with repositories, APIs, databases, and other systems that rely on async/await operations.

Unset

public ICommand SignInCommand { get; }

SignInCommand = new AsyncRelayCommand(async () => await SignInAsync());
or
SignInCommand = new AsyncRelayCommand(SignInAsync);

Dependency injection

Dependency injection, a specialized form of the Inversion of Control (IoC) pattern, focuses on acquiring necessary dependencies by delegating the responsibility of injecting these dependencies into an object at runtime to another class.

The code example below demonstrates the structure of the ProfileViewModel class when utilizing dependency injection:

Unset

public partial class LoginViewModel : BaseViewModel
{
private readonly IRestApiService _restApiService;
private readonly INavigationService _navigationService;
private readonly IFingerprint _fingerPrintService;
private readonly IPopupNavigation _popupService;
private readonly IAlertService _alertService;

public LoginViewModel(IRestApiService restApiService,
INavigationService navigationService,
IFingerprint fingerPrintService,
IPopupNavigation popupService,
IAlertService alertService)
{
_restApiService = restApiService;
_navigationService = navigationService;
_fingerPrintService = fingerPrintService;
_popupService = popupService;
_alertService = alertService;
}
…..
}

Using a dependency injection container offers several advantages:

– It eliminates the need for a class to locate its dependencies and manage its lifetimes.

– It allows dependencies to be mapped without affecting the class.

– It facilitates testability by enabling the mocking of dependencies.

– It enhances maintainability by making it easy to add new classes to the application.

In a .NET MAUI app utilizing MVVM, a dependency injection container is generally employed to register and resolve views and view models, as well as to register services and inject them into view models.

The following code example demonstrates how the MAUI app declares the CreateMauiApp method in the MauiProgram class:

Unset

public static class MauiProgram
{
public static MauiApp CreateMauiApp()
=> MauiApp.CreateBuilder()
.UseMauiApp()
// Omitted for brevity
.RegisterAppServices()
.RegisterViewModels()
.RegisterViews()
.Build();
}

RegisterViewModels, and RegisterViews were created to help provide an organized and maintainable registration workflow. The following code shows the RegisterViewModels method:

Unset

public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuilder)
{
mauiAppBuilder.Services.AddSingleton();
mauiAppBuilder.Services.AddSingleton();
mauiAppBuilder.Services.AddSingleton();
mauiAppBuilder.Services.AddSingleton();
mauiAppBuilder.Services.AddSingleton();

mauiAppBuilder.Services.AddTransient();
mauiAppBuilder.Services.AddTransient();

return mauiAppBuilder;
}

Unset

public static MauiAppBuilder RegisterViews(this MauiAppBuilder mauiAppBuilder)
{
mauiAppBuilder.Services.AddSingleton();
mauiAppBuilder.Services.AddSingleton();
mauiAppBuilder.Services.AddSingleton();
mauiAppBuilder.Services.AddSingleton();
mauiAppBuilder.Services.AddSingleton();

mauiAppBuilder.Services.AddTransient();
mauiAppBuilder.Services.AddTransient();

return mauiAppBuilder;
}

The following table guides when to choose different registration lifetimes.

Method Description
AddSingleton It will instantiate the object once, ensuring its presence throughout the application’s lifespan.
AddTransient Each time the object is requested during resolution, a fresh instance is generated. Transient objects lack a predetermined lifespan but usually align with their host’s duration.

– .NET MAUI supports both automatic and explicit dependency resolution. Automatic dependency resolution uses constructor injection without explicitly requesting dependencies from the container. Explicit dependency resolution happens on demand by directly requesting a dependency from the container.

– In this example, the MainPage constructor receives an injected MainPageViewModel instance. The MainPageViewModel instance, in turn, has ILoggingService and ISettingsService instances injected.

Unset

public class MainPageViewModel
{
readonly ILoggingService _loggingService;
readonly ISettingsService _settingsService;

public MainPageViewModel(ILoggingService loggingService, ISettingsService settingsService)
{
_loggingService = loggingService;
_settingsService = settingsService;
}
}

The dependency injection container can be explicitly accessed from an Element through its Handler.MauiContext.Service property, which is of type IServiceProvider:

Unset

public class MainPageViewModel
{
readonly ILoggingService _loggingService;
readonly ISettingsService _settingsService;

public MainPageViewModel()
{
_loggingService = Application.Current.MainPage.Handler.MauiContext.Services.GetService();
_settingsService = Application.Current.MainPage.Handler.MauiContext.Services.GetService();
}
}

In summary, dependency injection decouples concrete types from the code that depends on them. It typically involves a container that maintains a list of registrations and mappings between interfaces or abstract types and the concrete types that implement or extend them.

Messenger between components

The publish-subscribe pattern is a messaging model where publishers send messages without being aware of the receivers, known as subscribers. Likewise, subscribers listen for specific messages without knowing the publishers.

In .NET, events embody the publish-subscribe pattern and serve as the straightforward method for inter-component communication when loose coupling isn’t necessary, like between a control and its containing page. Nonetheless, memory management issues may arise from the lifetimes of publishers and subscribers, particularly when short-lived objects subscribe to events of static or long-lived objects.

Prior to .NET 7, we used the Messaging Center publish-subscribe pattern. With the release of .NET 7, Messaging Center has been deprecated and replaced by IMessenger in the CommunityToolkit.Mvvm.

The IMessenger interface outlines the publish-subscribe pattern, facilitating message-driven communication between components that are challenging to connect through object and type references.

The IMessenger interface enables multicast functionality for publish-subscribe operations. This implies that multiple publishers can send a single message, while multiple subscribers can listen to that same message.

There are two implementations of the IMessenger interface that come with the CommunityToolkit.Mvvm package.

– The WeakReferenceMessenger employs weak references, making it easier to clean up message subscribers. This is an ideal choice if your subscribers lack a clearly defined lifecycle.
– The StrongReferenceMessenger utilizes strong references, leading to improved performance and a more precisely managed subscription lifetime.

Define Message:

– IMessenger messages consist of custom objects carrying personalized payloads.

   Unset

public class AddPodcastMessage : ValueChangedMessage
{
public AddPodcastMessage(int count) : base(count)
{
}
}

Publishing a message

– To broadcast a message, we utilize the IMessenger.Send method, typically accessible via WeakReferenceMessenger.Default.Send or StrongReferenceMessenger.Default.Send. Messages can encompass any object type.

Unset

WeakReferenceMessenger.Default.Send(new Messages.AddPodcastMessage(PopcastCount));

– The Send method publishes the message and its payload using a fire-and-forget approach. As a result, the message is sent even if no subscribers are registered to receive it, and in such cases, the message is simply ignored.

Subscribing to a message

– Subscribers can register to receive messages by using one of the IMessenger.Register overloads.

Unset

WeakReferenceMessenger.Default
.Register(
this,
(recipient, message) =>
{
……
});

– If payload data is provided, avoid modifying it within a callback delegate since multiple threads may simultaneously access the data. To prevent concurrency errors, the payload data should be immutable.

Unsubscribing from a message payload:

– Subscribers can opt out of receiving messages they no longer wish to receive using one of the IMessenger.Unregister overloads, as shown in the following code example:

Unset

// Unregisters the recipient from a message type
WeakReferenceMessenger.Default.Unregister(this);

// Unregisters the recipient from a message type in a specified channel
WeakReferenceMessenger.Default.Unregister(this, 42);

// Unregister the recipient from all messages, across all channels
WeakReferenceMessenger.Default.UnregisterAll(this);

Summary: Messenger facilitates message-based communication between components that are difficult to connect through object and type references. This mechanism enables publishers and subscribers to interact without direct references to each other, reducing dependencies between components and allowing for independent development and testing.

Assessing remote data

Many modern web-based solutions utilize web services hosted on web servers to deliver functionality to remote client applications. The operations are exposed by a web service from a web API.

Client apps should be able to use the web API without needing to understand the implementation details of the data or operations it exposes.

Making the HTTP Requests:

– The MAUI application employs the HttpClient class to send HTTP requests, utilizing JSON as the media type.

– This class enables asynchronous sending of HTTP requests and receiving of HTTP responses from a resource identified by a URI.

Unset

private readonly Lazy _httpClient =
new Lazy(
() =>
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(“application/json”));
return httpClient;
},
LazyThreadSafetyMode.ExecutionAndPublication);

private HttpClient PrepareRestRequest(AuthenticateModel auth)
{
var httpClient = _httpClient.Value;
if (auth != null)
{
var authenticationString = $”{auth.Email}:{auth.Password}”;
var base64String = Convert.ToBase64String(
Encoding.ASCII.GetBytes(authenticationString));
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(“Basic”, base64String);
}
else
{
httpClient.DefaultRequestHeaders.Authorization = null;
}
return httpClient;
}

To improve application performance, it is crucial to cache and reuse instances of HttpClient. Creating a new HttpClient for each operation can cause socket exhaustion issues.

Making a GET request:

Unset

public async Task> GetAsync(AuthenticateModel auth, Uri uri)
{
var client = PrepareRestRequest(auth);
var result = new ResponseModel
{
Data = default,
IsSucceed = false,
};
try
{
HttpResponseMessage response = await client.GetAsync(uri);
result.IsSucceed = response.IsSuccessStatusCode;
result.StatusCode = (int)response.StatusCode;
string content = await response.Content.ReadAsStringAsync();
result.Data = JsonSerializer.Deserialize(content, _deSerializerOptions);
}
catch (Exception ex)
{
result.ErrorMessage = ex.Message;
result.StatusCode = 500;
}

return result;
}

– Making a POST request:

Unset

public async Task> PostAsync(AuthenticateModel? auth, Uri uri)
{
var client = PrepareRestRequest(auth);
var result = new ResponseModel
{
Data = default,
IsSucceed = false,
};
try
{
string json = JsonSerializer.Serialize(item, _serializerOptions);
var content = new StringContent(json, Encoding.UTF8, “application/json”);

HttpResponseMessage response = await client.PostAsync(uri, content);
result.IsSucceed = response.IsSuccessStatusCode;
result.StatusCode = (int)response.StatusCode;
string message = await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
result.ErrorMessage = ex.Message;
result.StatusCode = 500;
}

return result;
}

Making a DELETE Request

Unset

public async Task> DeleteAsync(AuthenticateModel? auth, Uri uri)
{
var client = PrepareRestRequest(auth);
var result = new ResponseModel
{
Data = default,
IsSucceed = false,
};
try
{
string json = JsonSerializer.Serialize(item, _serializerOptions);
var content = new StringContent(json, Encoding.UTF8, “application/json”);

HttpResponseMessage response = await client.DeleteAsync(uri, content);
result.IsSucceed = response.IsSuccessStatusCode;
result.StatusCode = (int)response.StatusCode;
}
catch (Exception ex)
{
result.ErrorMessage = ex.Message;
result.StatusCode = 500;
}

return result;
}

If an app detects a failure when attempting to send a request to a remote service, it can handle the failure in any of the following ways:

– Retrying the operation: The app can immediately retry the failed request.

– Retrying the operation after a delay: The app should wait for an appropriate amount of time before attempting the request again.
– Cancelling the operation: The application should cancel the request and report an exception.

The retry pattern should be tailored to the app’s business requirements. For instance, it’s crucial to fine-tune the retry count and interval for the specific operation being performed.

Implementing the Retry pattern using Polly in a .NET Core Web application enhances the application’s resilience by handling transient faults gracefully.

Unset

var retryPolicy = Policy
.Handle()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(15);
var result =  await retryPolicy.ExecuteAsync(async () =>
{
var response = await httpClient.GetAsync(“https://example.api.com/example-data”);

// Check if the request was successful
response.EnsureSuccessStatusCode();

// Process the response if successful
var content = await response.Content.ReadAsStringAsync();
return content;
});

Validation

Any app that accepts user input must ensure its validity. For example, the app could verify that the input contains only characters within a specific range, meets a certain length or adheres to a particular format.

Lack of validation opens the door for users to input data that can lead to app failures. Effective validation enforces business rules and serves as a defense against potential injection of malicious data by attackers.

In the Model-View-ViewModel (MVVM) pattern, it’s common for a view model or model to handle data validation and communicate any validation errors to the view, enabling users to rectify them.

First Approach: Using Observable Validator in MVVM toolkit.

– The ObservableValidator is a base class implementing the INotifyDataErrorInfo interface, providing support for validating properties exposed to other application modules.

– It also inherits from ObservableObject, so it implements INotifyPropertyChanged and INotifyPropertyChanging as well.

Unset

public class RegistrationForm : ObservableValidator
{
private string name;

[Required]
[MinLength(2)]
[MaxLength(100)]
public string Name
{
get => name;
set => SetProperty(ref name, value, true);
}
}

ObservableValidator showcases the following primary functionalities:

– It furnishes a foundational implementation for INotifyDataErrorInfo, offering access to the ErrorsChanged event and other essential APIs.

– It offers the ValidateProperty method, allowing for manual triggering of property validation. This is particularly handy when a property’s validation relies on another property’s value, which may have been updated while the former remains unchanged.

– It provides the ValidateAllProperties method, which automatically validates all public instance properties within the current instance. This occurs only if they are adorned with at least one [ValidationAttribute].

– It features a ClearAllErrors method, handy for resetting a model linked to a form that users may need to refill.

Customizing Validation Techniques.

– An alternative approach to custom validation involves crafting a custom [ValidationAttribute] and embedding the validation logic within the overridden IsValid method.

Unset

public sealed class GreaterThanAttribute : ValidationAttribute
{
public GreaterThanAttribute(string propertyName)
{
PropertyName = propertyName;
}

public string PropertyName { get; }

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
object
instance = validationContext.ObjectInstance,
otherValue = instance.GetType().GetProperty(PropertyName).GetValue(instance);

if (((IComparable)value).CompareTo(otherValue) > 0)
{
return ValidationResult.Success;
}

return new(“The current value is smaller than the other one”);
}
}

Next, we can incorporate this attribute into our view model.

Unset

public class ComparableModel : ObservableValidator

{
private int a;

[Range(10, 100)]
[GreaterThan(nameof(B))]
public int A
{
get => this.a;
set => SetProperty(ref this.a, value, true);
}

private int b;

[Range(20, 80)]
public int B
{
get => this.b;
set
{
SetProperty(ref this.b, value, true);
ValidateProperty(A, nameof(A));
}
}
}

Second Approach: Customizing ValidableObject

– Properties in the view model that necessitate validation are of type ValidatableObject. Each instance of ValidatableObject has its validation rules appended to its Validations property.

– Validation in the view model is triggered by invoking the Validate method of the ValidatableObject instance. This method retrieves the validation rules and applies them to the ValidatableObject.Value property.

– Validation errors are stored in the Errors property of the ValidatableObject instance, while the IsValid property of the ValidatableObject instance is updated to reflect the success or failure of the validation.
Validation - Deep Dive on MAUI Application

The following code shows the implementation of the ValidatableObject:

Unset

using CommunityToolkit.Mvvm.ComponentModel;
public class ValidatableObject : ObservableObject, IValidity
{
private IEnumerable _errors;
private bool _isValid;
private T _value;
public List> Validations { get; } = new();
public IEnumerable Errors
{
get => _errors;
private set => SetProperty(ref _errors, value);
}
public bool IsValid
{
get => _isValid;
private set => SetProperty(ref _isValid, value);
}
public T Value

{
get => _value;
set => SetProperty(ref _value, value);
}
public ValidatableObject()
{
_isValid = true;
_errors = Enumerable.Empty();
}
public bool Validate()
{
Errors = Validations
?.Where(v => !v.Check(Value))
?.Select(v => v.ValidationMessage)
?.ToArray()
?? Enumerable.Empty();
IsValid = !Errors.Any();
return IsValid;
}
}

Validation rules are specified by creating a class that derives from the IValidationRule interface

Unset

public interface IValidationRule
{
string ValidationMessage { get; set; }
bool Check(T value);
}

– This interface mandates that a validation rule class must include a boolean Check method for performing the necessary validation, and a ValidationMessage property to hold the error message displayed if validation fails.

– The following code example shows the IsNotNullOrEmptyRule validation rule.

Unset

public class EmailRule : IValidationRule
{
private readonly Regex _regex = new(@”^([w.-]+)@([w-]+)((.(w){2,3})+)$”);

public string ValidationMessage { get; set; }

public bool Check(T value) =>
value is string str && _regex.IsMatch(str);
}

So how  to add validation rules to a property in the view model:

Step 1: Declare the validated property in the view model. It needs to be type ValidatebleObject.

Unset

public ValidatableObject Email{ get; private set; }

Step 2: Add validation rules to the Validations collection of ValidateObject instance.

Unset

private void AddValidations()
{

Email.Validations.Add(new EmailRule
{
ValidationMessage = “A email is not correct.”
});
}
});

Step 3: Trigger validation when property changes:

Unset

<Entry Text=”{Binding Email.Value}”>
<Entry.Behaviors>
<behaviors:EventToCommandBehavior
EventName=”TextChanged”
Command=”{Binding ValidateEmailCommand}” />
</Entry.Behaviors>
</Entry>

Unset

private bool ValidateEmail()
{
return Email.Validate();

}

Step 4: Display validation errors

Unset

<material:TextField Title=”Email”
Text=”{Binding Email.Value}”
Keyboard=”Email”
Style=”{x:StaticResource EntryStyle}”>
<material:TextField.Attachments>
<Image Source=”error_container.png” Margin=”0,0,10,0″
IsVisible=”{Binding Email.IsValid, Converter={StaticResource Key=InvertedBoolConverter}}”/>
</material:TextField.Attachments>
<material:TextField.Behaviors>
<mct:EventToCommandBehavior
EventName=”TextChanged”
Command=”{Binding ValidateEmailCommand}” />
</material:TextField.Behaviors>
<material:TextField.Triggers>
<DataTrigger
TargetType=”material:TextField”
Binding=”{Binding Email.IsValid}”
Value=”False”>
<Setter Property=”BorderColor”
Value=”#F55718″ />
<Setter Property=”TitleColor”
Value=”#F55718″ />
</DataTrigger>
</material:TextField.Triggers>
</material:TextField>

Unset

<Label Text=”{Binding Email.Errors, Converter={StaticResource ListToStringConverter}}”
IsVisible=”{Binding Email.Errors, Converter={StaticResource IsListNotNullOrEmptyConverter}}”
FontSize=”14″
TextColor=”#F55718″/>

In summary, we will have a completed flow like this screenshot:

Validation

Summary

This article highlights several challenges encountered when developing an enterprise application and offers solutions to address them. To ensure your app can be modified or extended over time, it’s crucial to design it with adaptability in mind. This involves partitioning the app into discrete, loosely coupled components that can be easily integrated. While challenging, this approach guarantees the app remains flexible and maintainable.

Thank you for reading this article. In the next one, I’ll cover topics such as authentication, platform invocation, how to apply async/await in mobile apps, and unit testing ….  to help you tackle the challenges of developing an enterprise application. Stay tuned!

Reference: 

MAUI Documentation: Link

MAUI blog: Link

MAUI tutorial channel: Link

contact

.NET Development Services

Optimize your software with our professional .NET development services, crafted to your specifications.
View Our Offerings arrow-narrow-right.png
Content manager
Thanh (Bruce) Pham
CEO of Saigon Technology
A Member of Forbes Technology Council

Related articles

Java or .NET For Web Application Development
Industry

Java or .NET For Web Application Development

Java or .NET for your web development project? Saigon Technology shares its 12-year expertise. Starting a new IT project can be tough, especially for beginners.
What You Need to Know about State Management: 6 Techniques for ASP.NET Core MVC
Methodology

What You Need to Know about State Management: 6 Techniques for ASP.NET Core MVC

This blog post covers 6 state management techniques for ASP.NET Core MVC. Saigon Technology, with 10+ years of .NET core experience, discussed them.
Unlocking the Potential of .NET MAUI for Frontend and Web Developers
Methodology

Unlocking the Potential of .NET MAUI for Frontend and Web Developers

Let our .net development company help you hire .net developers and unlock the potential of .NET MAUI. Begin your Web Application Development projects with .NET MAUI.
Build Customer Service (.NET, Minimal API, and PostgreSQL)
Methodology

Build Customer Service (.NET, Minimal API, and PostgreSQL)

In the previous section (part 2), I provided an overview of how to set up a working environment using Docker containers and build a Product Service. In this section (part 3), we will shift our focus to building Customer Service using .NET, Minimal API, and PostgreSQL.
calendar 13 Jun 2024
Say Hello to .NET MAUI
Methodology

Say Hello to .NET MAUI

This series introduces .NET MAUI and shows how to develop an application with it. The first article covers what .NET MAUI is, how it works, and its key features.
calendar 10 Apr 2024
How to build UI with .NET MAUI
Technologies

How to build UI with .NET MAUI

This article will help you learn how to create a UI with MAUI layouts and controls. It also gives some tips when developing applications with MAUI.
calendar 10 Apr 2024
Real-time ASP.NET with SignalR
Technologies

Real-time ASP.NET with SignalR

Today we are going to dive into SignalR for .NET. What is a real-time app, and how does SignalR work in .NET? Cover SignalR concepts and build a chat app.
calendar 19 Sep 2024

Want to stay updated on industry trends for your project?

We're here to support you. Reach out to us now.
Contact Message Box
Back2Top