Szkolenie
Ścieżka szkoleniowa
Implement finance and operations apps - Training
Plan and design your project methodology to successfully implement finance and operations apps with FastTrack services, data management and more.
Ta przeglądarka nie jest już obsługiwana.
Przejdź na przeglądarkę Microsoft Edge, aby korzystać z najnowszych funkcji, aktualizacji zabezpieczeń i pomocy technicznej.
Ten temat zawiera omówienie Managed Extensibility Framework, które zostały wprowadzone w .NET Framework 4.
Biblioteka Managed Extensibility Framework lub MEF to biblioteka do tworzenia lekkich i rozszerzalnych aplikacji. Umożliwia ona deweloperom aplikacji odnajdywanie i używanie rozszerzeń bez potrzeby konfigurowania. Umożliwia również deweloperom rozszerzeń łatwe hermetyzowanie kodu i uniknięcie niestabilnych twardych zależności. MeF nie tylko umożliwia ponowne użycie rozszerzeń w aplikacjach, ale również w aplikacjach.
Imagine, że jesteś architektem dużej aplikacji, która musi zapewniać obsługę rozszerzalności. Aplikacja musi zawierać potencjalnie dużą liczbę mniejszych składników i jest odpowiedzialna za ich tworzenie i uruchamianie.
Najprostszym podejściem do problemu jest dołącznie składników jako kodu źródłowego w aplikacji i wywołanie ich bezpośrednio z kodu. Ma to wiele oczywistych wad. Co najważniejsze, nie można dodawać nowych składników bez modyfikacji kodu źródłowego, czyli ograniczenia, które może być akceptowalne na przykład w aplikacji internetowej, ale nie jest możliwe w aplikacji klienckiej. Równie problematyczne może być to, że nie masz dostępu do kodu źródłowego składników, ponieważ mogą one być opracowywane przez inne firmy i z tego samego powodu nie możesz zezwolić im na dostęp do Twoich składników.
Nieco bardziej zaawansowane podejście polegałoby na udostępnieniu punktu rozszerzenia lub interfejsu, aby umożliwić rozdzielenie między aplikacją i jej składnikami. W ramach tego modelu możesz udostępnić interfejs, który składnik może zaimplementować, oraz interfejs API umożliwiający interakcję z aplikacją. Rozwiązuje to problem wymagania dostępu do kodu źródłowego, ale nadal ma własne trudności.
Ponieważ aplikacja nie ma żadnej pojemności do odnajdywania składników samodzielnie, nadal musi być jawnie określona, które składniki są dostępne i powinny zostać załadowane. Zwykle jest to realizowane przez jawne zarejestrowanie dostępnych składników w pliku konfiguracji. Oznacza to, że zapewnianie poprawności składników staje się problemem z konserwacją, szczególnie jeśli to użytkownik końcowy, a nie deweloper, który powinien wykonać aktualizację.
Ponadto składniki nie mogą komunikować się ze sobą, z wyjątkiem ściśle zdefiniowanych kanałów samej aplikacji. Jeśli architekt aplikacji nie przewidział potrzeby konkretnej komunikacji, zwykle jest to niemożliwe.
Na koniec deweloperzy składników muszą zaakceptować trudną zależność od tego, jaki zestaw zawiera implementny interfejs. Utrudnia to stosowanie składnika w więcej niż jednej aplikacji, a także może tworzyć problemy podczas tworzenia struktury testowej dla składników.
Zamiast tej jawnej rejestracji dostępnych składników mef umożliwia ich niejawne odnajdywanie za pośrednictwem kompozycji. Składnik MEF, nazywany częścią, deklaratywnie określa zarówno jego zależności (nazywane importami ), jak i możliwości (nazywane eksportami ), które udostępnia. Po utworzeniu części aparat kompozycji MEF spełnia swoje warunki importu z tym, co jest dostępne z innych części.
To podejście rozwiązuje problemy omówione w poprzedniej sekcji. Ponieważ części MEF deklaratywnie określają swoje możliwości, są wykrywalne w czasie rzeczywistym, co oznacza, że aplikacja może korzystać z części bez zakodowanych odwołań lub niestabilnych plików konfiguracji. MeF umożliwia aplikacjom odnajdywanie i badanie części według ich metadanych bez ich wystąpienia lub nawet ładowania zestawów. W związku z tym nie trzeba dokładnie określać, kiedy i jak mają być ładowane rozszerzenia.
Oprócz dostarczonych eksportów część może określić swoje importy, które zostaną wypełnione przez inne części. Dzięki temu komunikacja między częściami jest nie tylko możliwa, ale i łatwa, a także umożliwia dobre faktorowanie kodu. Na przykład usługi wspólne dla wielu składników mogą być w oddzielnej części i łatwo modyfikowane lub zastępowane.
Ponieważ model MEF nie wymaga stałej zależności od określonego zestawu aplikacji, umożliwia ponowne użycie rozszerzeń z aplikacji do aplikacji. Ułatwia to również opracowywanie aplikacji testowej, niezależnie od aplikacji, do testowania składników rozszerzenia.
Rozszerzalna aplikacja napisana przy użyciu mef deklaruje import, który może zostać wypełniony przez składniki rozszerzenia, a także może deklarować eksporty w celu uwidocznić usługi aplikacji dla rozszerzeń. Każdy składnik rozszerzenia deklaruje eksport i może również deklarować importy. W ten sposób same składniki rozszerzenia są automatycznie rozszerzalne.
MEF jest integralną częścią .NET Framework 4 i jest dostępny wszędzie tam, gdzie .NET Framework jest używany. MeF można używać w aplikacjach klienckich, niezależnie od tego, czy używają Windows Forms, WPF, czy innej technologii, albo w aplikacjach serwerowych, które używają ASP.NET.
W poprzednich wersjach .NET Framework wprowadzono zarządzaną platformę dodatku (MAF), która umożliwia aplikacjom izolowanie rozszerzeń i zarządzanie nimi. Głównym celem programu MAF jest nieco wyższy poziom niż MEF, który koncentruje się na izolacji rozszerzeń oraz ładowaniu i zwalnianiu zestawu, a mef koncentruje się na odnajdywaniu, rozszerzalności i przenośności. Te dwie struktury bezproblemowo współdziałają, a jedna aplikacja może korzystać z obu tych możliwości.
Najprostszym sposobem, aby zobaczyć, co może zrobić mef, jest skompilowanie prostej aplikacji MEF. W tym przykładzie tworzysz bardzo prosty kalkulator o nazwie SimpleCalculator. Celem simpleCalculator jest utworzenie aplikacji konsolowej, która akceptuje podstawowe polecenia arytmetyczne w postaci "5+3" lub "6-2" i zwraca prawidłowe odpowiedzi. Korzystając z funkcji MEF, będziesz mieć możliwość dodawania nowych operatorów bez zmiany kodu aplikacji.
Aby pobrać pełny kod dla tego przykładu, zobacz przykład SimpleCalculator (Visual Basic).
Uwaga
Celem simpleCalculator jest zademonstrowanie koncepcji i składni mef, a nie konieczności zapewnienia realistycznego scenariusza jego użycia. Wiele aplikacji, które najbardziej skorzystają na możliwościach mef, jest bardziej złożonych niż SimpleCalculator. Aby uzyskać bardziej rozbudowane przykłady, zobacz Managed Extensibility Framework na GitHub.
Aby rozpocząć, Visual Studio utwórz nowy projekt aplikacja konsolowa i nadaj jej nazwę SimpleCalculator
.
Dodaj odwołanie do zestawu System.ComponentModel.Composition
, w którym znajduje się mef.
Otwórz moduł Module1.vb lub Program.cs i dodaj Imports
instrukcje lub using
dla i System.ComponentModel.Composition.Hosting
System.ComponentModel.Composition
. Te dwie przestrzenie nazw zawierają typy MEF potrzebne do opracowania rozszerzalnych aplikacji.
Jeśli używasz polecenia Visual Basic, dodaj Public
słowo kluczowe do wiersza, który deklaruje Module1
moduł.
Podstawą modelu kompozycji MEF jest kontener kompozycji, który zawiera wszystkie dostępne części i wykonuje kompozycję. Kompozycja to dopasowanie importów do eksportów. Najpopularniejszym typem kontenera kompozycji jest CompositionContainer, a użyjesz go dla simpleCalculator.
Jeśli używasz klasy Visual Basic, dodaj klasę publiczną o nazwie w Program
module Module1.vb.
Dodaj następujący wiersz do klasy Program
w module1.vb lub Program.cs:
Dim _container As CompositionContainer
private CompositionContainer _container;
Aby odnaleźć dostępne dla niego części, kontenery kompozycji korzystają z katalogu. Wykaz jest obiektem, który udostępnia dostępne części odnalezione z jakiegoś źródła. MeF udostępnia wykazy do odnajdywania części z podanego typu, zestawu lub katalogu. Deweloperzy aplikacji mogą łatwo tworzyć nowe katalogi w celu odnajdywania części z innych źródeł, takich jak usługa internetowa.
Dodaj następujący konstruktor do Program
klasy :
Public Sub New()
' An aggregate catalog that combines multiple catalogs.
Dim catalog = New AggregateCatalog()
' Adds all the parts found in the same assembly as the Program class.
catalog.Catalogs.Add(New AssemblyCatalog(GetType(Program).Assembly))
' Create the CompositionContainer with the parts in the catalog.
_container = New CompositionContainer(catalog)
' Fill the imports of this object.
Try
_container.ComposeParts(Me)
Catch ex As CompositionException
Console.WriteLine(ex.ToString)
End Try
End Sub
private Program()
{
try
{
// An aggregate catalog that combines multiple catalogs.
var catalog = new AggregateCatalog();
// Adds all the parts found in the same assembly as the Program class.
catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));
// Create the CompositionContainer with the parts in the catalog.
_container = new CompositionContainer(catalog);
_container.ComposeParts(this);
}
catch (CompositionException compositionException)
{
Console.WriteLine(compositionException.ToString());
}
}
Wywołanie w celu nakazuje ComposeParts kontenerowi kompozycji redagowanie określonego zestawu części, w tym przypadku bieżącego wystąpienia klasy Program
. Jednak na tym etapie nic się nie stanie, ponieważ nie Program
ma żadnych importów do wypełnienia.
Najpierw zaimportujesz Program
kalkulator. Umożliwia to rozdzielenie kwestii dotyczących interfejsu użytkownika, Program
takich jak dane wejściowe i wyjściowe konsoli, które będą trafiać do interfejsu , z logiki kalkulatora.
Dodaj następujący kod do Program
klasy :
<Import(GetType(ICalculator))>
Public Property calculator As ICalculator
[Import(typeof(ICalculator))]
public ICalculator calculator;
Zwróć uwagę, że deklaracja obiektu nie calculator
jest niczym nietypowym, ale jest dekorowany za pomocą atrybutu ImportAttribute . Ten atrybut deklaruje coś jako import; oznacza to, że zostanie on wypełniony przez aparat kompozycji, gdy obiekt jest skomponowany.
Każdy import ma kontrakt, który określa, jakie eksporty zostaną dopasowane. Kontrakt może być jawnie określonym ciągiem lub może być automatycznie generowany przez mef z danego typu, w tym przypadku interfejsu ICalculator
. Każdy eksport zadeklarowany za pomocą pasującego kontraktu spowoduje zrealizowanie tego importu. Należy pamiętać, że chociaż typ obiektu calculator
jest w rzeczywistości ICalculator
, nie jest to wymagane. Kontrakt jest niezależny od typu obiektu importowanego. (W tym przypadku możesz pozostawić . typeof(ICalculator)
MeF automatycznie zakłada, że kontrakt jest oparty na typie importu, chyba że zostanie określony jawnie.
Dodaj ten bardzo prosty interfejs do modułu lub przestrzeni nazw SimpleCalculator
:
Public Interface ICalculator
Function Calculate(input As String) As String
End Interface
public interface ICalculator
{
string Calculate(string input);
}
Teraz, gdy zdefiniowano klasę ICalculator
, potrzebujesz klasy, która ją implementuje. Dodaj następującą klasę do modułu lub przestrzeni SimpleCalculator
nazw:
<Export(GetType(ICalculator))>
Public Class MySimpleCalculator
Implements ICalculator
End Class
[Export(typeof(ICalculator))]
class MySimpleCalculator : ICalculator
{
}
Oto eksport, który będzie odpowiadać importowi w pliku Program
. Aby eksport był zgodne z importem, eksport musi mieć ten sam kontrakt. Eksportowanie w ramach kontraktu opartego typeof(MySimpleCalculator)
na programie wywłaszłoby niezgodność i import nie zostałby wypełniony; kontrakt musi być dokładnie taki sam.
Ponieważ kontener kompozycji zostanie wypełniony wszystkimi częściami dostępnymi w tym zestawie, MySimpleCalculator
część będzie dostępna. Gdy konstruktor dla wykonuje Program
kompozycję na Program
obiekcie, jego import MySimpleCalculator
zostanie wypełniony obiektem, który zostanie utworzony w tym celu.
Warstwa interfejsu użytkownika (Program
) nie musi wiedzieć nic innego. W związku z tym możesz wypełnić pozostałą część logiki interfejsu użytkownika w Main
metodzie .
Dodaj następujący kod do metody Main
:
Sub Main()
' Composition is performed in the constructor.
Dim p As New Program()
Dim s As String
Console.WriteLine("Enter Command:")
While (True)
s = Console.ReadLine()
Console.WriteLine(p.calculator.Calculate(s))
End While
End Sub
static void Main(string[] args)
{
// Composition is performed in the constructor.
var p = new Program();
Console.WriteLine("Enter Command:");
while (true)
{
string s = Console.ReadLine();
Console.WriteLine(p.calculator.Calculate(s));
}
}
Ten kod po prostu odczytuje wiersz danych wejściowych i Calculate
ICalculator
wywołuje funkcję w wyniku, którą zapisuje z powrotem do konsoli. To jest cały kod, który jest potrzebny w .Program
Cała pozostała część pracy będzie odbywać się w częściach.
Aby simpleCalculator był rozszerzalny, musi zaimportować listę operacji. Zwykły atrybut ImportAttribute jest wypełniany przez jeden i tylko jeden .ExportAttribute Jeśli jest dostępnych więcej niż jeden, aparat kompozycji generuje błąd. Aby utworzyć import, który można wypełnić dowolną liczbą eksportów, można użyć atrybutu ImportManyAttribute .
Dodaj następującą właściwość operations do MySimpleCalculator
klasy :
<ImportMany()>
Public Property operations As IEnumerable(Of Lazy(Of IOperation, IOperationData))
[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;
Lazy<T,TMetadata> jest typem dostarczonym przez mef do przechowywania pośrednich odwołań do eksportów. W tym miejscu oprócz wyeksportowanego obiektu można również uzyskać metadane eksportu lub informacje opisujące wyeksportowany obiekt. Każdy Lazy<T,TMetadata> obiekt zawiera IOperation
obiekt reprezentujący rzeczywistą operację i IOperationData
obiekt reprezentujący jego metadane.
Dodaj następujące proste interfejsy do modułu lub przestrzeni nazw SimpleCalculator
:
Public Interface IOperation
Function Operate(left As Integer, right As Integer) As Integer
End Interface
Public Interface IOperationData
ReadOnly Property Symbol As Char
End Interface
public interface IOperation
{
int Operate(int left, int right);
}
public interface IOperationData
{
char Symbol { get; }
}
W tym przypadku metadane dla każdej operacji są symbolem reprezentującym tę operację, taką jak +, -, *i tak dalej. Aby udostępnić operację dodawania, dodaj następującą klasę do modułu lub przestrzeni nazw SimpleCalculator
:
<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "+"c)>
Public Class Add
Implements IOperation
Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
Return left + right
End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '+')]
class Add: IOperation
{
public int Operate(int left, int right)
{
return left + right;
}
}
Atrybut ExportAttribute działa tak jak wcześniej. Atrybut ExportMetadataAttribute dołącza metadane w postaci pary nazwa-wartość do tego eksportu. Klasa implementuje Add
klasę IOperation
, która implementuje IOperationData
klasę , ale nie jest jawnie zdefiniowana. Zamiast tego klasa jest niejawnie tworzona przez mef z właściwościami opartymi na nazwach podanych metadanych. (Jest to jeden z kilku sposobów uzyskiwania dostępu do metadanych w mef).
Kompozycja w mef jest rekursywna. Jawnie skomponujesz Program
obiekt , który zaimportował ICalculator
obiekt , który okazał się typem MySimpleCalculator
. MySimpleCalculator
z kolei importuje kolekcję IOperation
obiektów, MySimpleCalculator
a import zostanie wypełniony podczas tworzenia w tym samym czasie co import .Program
Jeśli klasa Add
zadeklarowała dalsze importowanie, to również musiałoby zostać wypełnione i tak dalej. Każdy import pozostawiony jako niezapełniony powoduje błąd kompozycji. (Można jednak zadeklarować importy jako opcjonalne lub przypisać im wartości domyślne).
Gdy te części są już dostępne, pozostaje tylko logika kalkulatora. Dodaj następujący kod w klasie MySimpleCalculator
, aby zaimplementować Calculate
metodę :
Public Function Calculate(input As String) As String Implements ICalculator.Calculate
Dim left, right As Integer
Dim operation As Char
' Finds the operator.
Dim fn = FindFirstNonDigit(input)
If fn < 0 Then
Return "Could not parse command."
End If
operation = input(fn)
Try
' Separate out the operands.
left = Integer.Parse(input.Substring(0, fn))
right = Integer.Parse(input.Substring(fn + 1))
Catch ex As Exception
Return "Could not parse command."
End Try
For Each i As Lazy(Of IOperation, IOperationData) In operations
If i.Metadata.symbol = operation Then
Return i.Value.Operate(left, right).ToString()
End If
Next
Return "Operation not found!"
End Function
public String Calculate(string input)
{
int left;
int right;
char operation;
// Finds the operator.
int fn = FindFirstNonDigit(input);
if (fn < 0) return "Could not parse command.";
try
{
// Separate out the operands.
left = int.Parse(input.Substring(0, fn));
right = int.Parse(input.Substring(fn + 1));
}
catch
{
return "Could not parse command.";
}
operation = input[fn];
foreach (Lazy<IOperation, IOperationData> i in operations)
{
if (i.Metadata.Symbol.Equals(operation))
{
return i.Value.Operate(left, right).ToString();
}
}
return "Operation Not Found!";
}
Początkowe kroki analizują ciąg wejściowy na lewy i prawy operand oraz znak operatora. W pętli foreach
sprawdzany jest każdy operations
członek kolekcji. Te obiekty są typu Lazy<T,TMetadata>, a ich wartości metadanych MetadataValue i wyeksportowany obiekt są dostępne odpowiednio za pomocą właściwości i . W takim przypadku, jeśli Symbol
IOperationData
właściwość obiektu zostanie odnaleziona jako dopasowanie, Operate
IOperation
kalkulator wywoła metodę obiektu i zwróci wynik.
Do ukończenia kalkulatora potrzebna jest również metoda pomocnika, która zwraca pozycję pierwszego znaku niecyfrowego w ciągu. Dodaj następującą metodę pomocnika do MySimpleCalculator
klasy :
Private Function FindFirstNonDigit(s As String) As Integer
For i = 0 To s.Length - 1
If Not Char.IsDigit(s(i)) Then Return i
Next
Return -1
End Function
private int FindFirstNonDigit(string s)
{
for (int i = 0; i < s.Length; i++)
{
if (!char.IsDigit(s[i])) return i;
}
return -1;
}
Teraz powinno być możliwe skompilowanie i uruchomienie projektu. W Visual Basic upewnij się, że dodano słowo kluczowe Public
do .Module1
W oknie konsoli wpisz operację dodatku, taką jak "5+3", a kalkulator zwróci wyniki. Każdy inny operator powoduje komunikat "Nie znaleziono operacji!".
Teraz, gdy kalkulator działa, dodawanie nowej operacji jest łatwe. Dodaj następującą klasę do modułu lub przestrzeni SimpleCalculator
nazw:
<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "-"c)>
Public Class Subtract
Implements IOperation
Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
Return left - right
End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '-')]
class Subtract : IOperation
{
public int Operate(int left, int right)
{
return left - right;
}
}
Skompiluj i uruchom projekt. Wpisz operację odejmowania, taką jak "5–3". Kalkulator obsługuje teraz odejmowanie, a także dodawanie.
Dodawanie klas do kodu źródłowego jest wystarczająco proste, ale mef zapewnia możliwość wyszukiwania części poza własnym źródłem aplikacji. Aby to zademonstrować, musisz zmodyfikować simpleCalculator, aby przeszukiwać katalog i jego własny zestaw dla części, dodając element DirectoryCatalog.
Dodaj nowy katalog o nazwie Extensions
do projektu SimpleCalculator. Pamiętaj, aby dodać go na poziomie projektu, a nie na poziomie rozwiązania. Następnie dodaj do rozwiązania nowy projekt Biblioteka klas o nazwie ExtendedOperations
. Nowy projekt zostanie skompilowany w osobnym zestawie.
Otwórz projektanta właściwości Project projektu ExtendedOperations i kliknij kartę Kompiluj lub Kompiluj. Zmień ścieżkę wyjściową kompilacji lub ścieżkę wyjściową, aby wskazać katalog Extensions w katalogu projektu SimpleCalculator (.. \SimpleCalculator\Extensions\).
W module1.vb lub Program.cs dodaj następujący wiersz do konstruktora Program
:
catalog.Catalogs.Add(
New DirectoryCatalog(
"C:\SimpleCalculator\SimpleCalculator\Extensions"))
catalog.Catalogs.Add(
new DirectoryCatalog(
"C:\\SimpleCalculator\\SimpleCalculator\\Extensions"));
Zastąp przykładową ścieżkę ścieżką do katalogu Extensions. (Ta ścieżka bezwzględna jest tylko do celów debugowania. W aplikacji produkcyjnej należy użyć ścieżki względnej). Element DirectoryCatalog doda teraz wszystkie części znalezione w dowolnych zestawach w katalogu Extensions do kontenera kompozycji.
W projekcie ExtendedOperations dodaj odwołania do elementów SimpleCalculator i System.ComponentModel.Composition. W pliku klasy ExtendedOperations dodaj Imports
instrukcje lub using
dla elementu System.ComponentModel.Composition. W Visual Basic dodaj również instrukcje Imports
dla simpleCalculator. Następnie dodaj następującą klasę do pliku klasy ExtendedOperations:
<Export(GetType(SimpleCalculator.IOperation))>
<ExportMetadata("Symbol", "%"c)>
Public Class Modulo
Implements IOperation
Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
Return left Mod right
End Function
End Class
[Export(typeof(SimpleCalculator.IOperation))]
[ExportMetadata("Symbol", '%')]
public class Mod : SimpleCalculator.IOperation
{
public int Operate(int left, int right)
{
return left % right;
}
}
Należy pamiętać, że aby kontrakt był taki sam, ExportAttribute atrybut musi mieć ten sam typ co ImportAttribute.
Skompiluj i uruchom projekt. Przetestuj nowy operator Mod (%) .
W tym temacie o pojęciach dotyczących mef.
Części, wykazy i kontener kompozycji
Części i kontener kompozycji to podstawowe bloki konstrukcyjne aplikacji MEF. Część to dowolny obiekt, który importuje lub eksportuje wartość, aż do i do samej siebie. Katalog udostępnia kolekcję części z określonego źródła. Kontener kompozycji używa części dostarczonych przez wykaz do wykonania kompozycji, czyli powiązania importów z eksportami.
Importy i eksporty
Importy i eksporty to sposób, w jaki składniki komunikują się. W przypadku importowania składnik określa potrzebę określonej wartości lub obiektu, a po wyeksportowaniu określa dostępność wartości. Każdy import jest do matched z listą eksportów w drodze jego kontraktu.
Aby pobrać pełny kod dla tego przykładu, zobacz przykład SimpleCalculator (Visual Basic).
Aby uzyskać więcej informacji i przykładów kodu, zobacz Managed Extensibility Framework. Aby uzyskać listę typów MEF, zobacz przestrzeń System.ComponentModel.Composition nazw .
Szkolenie
Ścieżka szkoleniowa
Implement finance and operations apps - Training
Plan and design your project methodology to successfully implement finance and operations apps with FastTrack services, data management and more.