Udostępnij za pośrednictwem


Używanie platformy ASP.NET Core SignalR z Blazor

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Ostrzeżenie

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz .NET i .NET Core Support Policy (Zasady obsługi platformy .NET Core). Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Ten samouczek zawiera podstawowe środowisko robocze do tworzenia aplikacji w czasie rzeczywistym przy użyciu programu SignalRBlazor. Ten artykuł jest przydatny dla deweloperów, którzy są już zaznajomieni i SignalR chcą zrozumieć, jak używać SignalR w Blazor aplikacji. Aby uzyskać szczegółowe wskazówki dotyczące platform SignalR i Blazor , zobacz następujące zestawy dokumentacji referencyjnej i dokumentację interfejsu API:

Instrukcje:

  • Tworzenie Blazor aplikacji
  • Dodawanie biblioteki SignalR klienta
  • SignalR Dodawanie koncentratora
  • Dodawanie SignalR usług i punktu końcowego SignalR dla centrum
  • Razor Dodawanie kodu składnika na potrzeby czatu

Na końcu tego samouczka będziesz mieć działającą aplikację do czatu.

Wymagania wstępne

Program Visual Studio (najnowsza wersja) z pakietem roboczym tworzenia aplikacji internetowych i ASP.NET

Przykładowa aplikacja

Pobranie przykładowej aplikacji do czatu z samouczka nie jest wymagane w tym samouczku. Przykładowa aplikacja to ostateczna, działająca aplikacja utworzona przez wykonanie kroków tego samouczka. Po otwarciu repozytorium przykładów otwórz folder wersji, który ma być docelowy, i znajdź przykład o nazwie BlazorSignalRApp.

Pobranie przykładowej aplikacji do czatu z samouczka nie jest wymagane w tym samouczku. Przykładowa aplikacja to ostateczna, działająca aplikacja utworzona przez wykonanie kroków tego samouczka. Po otwarciu repozytorium przykładów otwórz folder wersji, który ma być docelowy, i znajdź przykład o nazwie BlazorWebAssemblySignalRApp.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Tworzenie Blazor aplikacji internetowej

Postępuj zgodnie ze wskazówkami dotyczącymi wyboru narzędzi:

Uwaga

Wymagany jest program Visual Studio 2022 lub nowszy oraz zestaw .NET Core SDK 8.0.0 lub nowszy.

W programie Visual Studio:

  • Wybierz pozycję Utwórz nowy projekt w oknie startowym lub wybierz pozycję Plik>nowy>projekt na pasku menu.
  • W oknie dialogowym Tworzenie nowego projektu wybierz pozycję Blazor Aplikacja internetowa z listy szablonów projektów. Kliknij przycisk Next (Dalej).
  • W oknie dialogowym Konfigurowanie nowego projektu nadaj projektowi nazwę projektu BlazorSignalRApp w polu Nazwa projektu, w tym dopasowanie liter. Użycie tej dokładnej nazwy projektu jest ważne, aby upewnić się, że przestrzenie nazw pasują do kodu skopiowanego z samouczka do aplikacji, którą tworzysz.
  • Upewnij się, że lokalizacja aplikacji jest odpowiednia. Pozostaw zaznaczone pole wyboru Umieść rozwiązanie i projekt w tym samym katalogu. Kliknij przycisk Next (Dalej).
  • W oknie dialogowym Dodatkowe informacje użyj następujących ustawień:
    • Struktura: upewnij się, że wybrano najnowszą strukturę . Jeśli lista rozwijana programu Visual Studio Framework nie zawiera najnowszej dostępnej platformy .NET Framework , zaktualizuj program Visual Studio i uruchom ponownie samouczek.
    • Typ uwierzytelniania: Brak
    • Konfigurowanie dla protokołu HTTPS: wybrane
    • Tryb renderowania interakcyjnego: Zestaw WebAssembly
    • Lokalizacja interakcyjności: na stronę/składnik
    • Uwzględnij przykładowe strony: wybrane
    • Nie używaj instrukcji najwyższego poziomu: nie wybrano
    • Wybierz pozycję Utwórz.

Wskazówki zawarte w tym artykule używają składnika WebAssembly dla SignalR klienta, ponieważ nie ma sensu używać SignalR do nawiązywania połączenia z koncentratorem ze składnika Interactive Server w tej samej aplikacji, co może prowadzić do wyczerpania portów serwera.

Dodawanie biblioteki SignalR klienta

W Eksplorator rozwiązań kliknij prawym przyciskiem myszy BlazorSignalRApp.Client projekt i wybierz polecenie Zarządzaj pakietami NuGet.

W oknie dialogowym Zarządzanie pakietami NuGet upewnij się, że źródło pakietu ma wartość nuget.org.

Po wybraniu pozycji Przeglądaj wpisz Microsoft.AspNetCore.SignalR.Client w polu wyszukiwania.

W wynikach wyszukiwania wybierz najnowszą wersję Microsoft.AspNetCore.SignalR.Client pakietu. Wybierz Zainstaluj.

Jeśli zostanie wyświetlone okno dialogowe Podgląd zmian, wybierz przycisk OK.

Jeśli zostanie wyświetlone okno dialogowe Akceptacja licencji, wybierz pozycję Akceptuję, jeśli zgadzasz się z postanowieniami licencyjnymi.

SignalR Dodawanie koncentratora

W projekcie serwera BlazorSignalRApp utwórz folder (mnoga) i dodaj następującą ChatHub klasę Hubs (Hubs/ChatHub.cs):

using Microsoft.AspNetCore.SignalR;

namespace BlazorSignalRApp.Hubs;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

Dodawanie usług i punktu końcowego SignalR dla centrum

Program Otwórz plik projektu serweraBlazorSignalRApp.

Dodaj przestrzenie nazw i Microsoft.AspNetCore.ResponseCompression klasę ChatHub na początku pliku:

using Microsoft.AspNetCore.ResponseCompression;
using BlazorSignalRApp.Hubs;

Dodaj SignalR i usługi oprogramowania pośredniczącego kompresji odpowiedzi:

builder.Services.AddSignalR();

builder.Services.AddResponseCompression(opts =>
{
   opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
       ["application/octet-stream"]);
});

Użyj oprogramowania pośredniczącego kompresji odpowiedzi w górnej części konfiguracji potoku przetwarzania. Umieść następujący wiersz kodu bezpośrednio po wierszu, który kompiluje aplikację (var app = builder.Build();):

app.UseResponseCompression();

Dodaj punkt końcowy centrum bezpośrednio przed wierszem, który uruchamia aplikację (app.Run();):

app.MapHub<ChatHub>("/chathub");

Dodawanie Razor kodu składnika na potrzeby czatu

Dodaj następujący Pages/Chat.razor plik do BlazorSignalRApp.Client projektu:

@page "/chat"
@rendermode InteractiveWebAssembly
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager Navigation
@implements IAsyncDisposable

<PageTitle>Chat</PageTitle>

<div class="form-group">
    <label>
        User:
        <input @bind="userInput" />
    </label>
</div>
<div class="form-group">
    <label>
        Message:
        <input @bind="messageInput" size="50" />
    </label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private HubConnection? hubConnection;
    private List<string> messages = new List<string>();
    private string? userInput;
    private string? messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            InvokeAsync(StateHasChanged);
        });

        await hubConnection.StartAsync();
    }

    private async Task Send()
    {
        if (hubConnection is not null)
        {
            await hubConnection.SendAsync("SendMessage", userInput, messageInput);
        }
    }

    public bool IsConnected =>
        hubConnection?.State == HubConnectionState.Connected;

    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
}

Dodaj wpis do składnika, NavMenu aby uzyskać dostęp do strony czatu. Natychmiast Components/Layout/NavMenu.razor po <div> bloku składnika Weather dodaj następujący <div> blok:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="chat">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Chat
    </NavLink>
</div>

Uwaga

Wyłącz oprogramowanie pośredniczące kompresji odpowiedzi w Development środowisku podczas korzystania z Przeładowywanie na gorąco. Aby uzyskać więcej informacji, zobacz wskazówki dotyczące platformy ASP.NET CoreBlazorSignalR.

Uruchom aplikację

Postępuj zgodnie ze wskazówkami dotyczącymi narzędzi:

Po wybraniu projektu serwera BlazorSignalRApp w Eksplorator rozwiązań naciśnij F5, aby uruchomić aplikację z debugowaniem lub Ctrl+F5 (Windows)/+F5 (macOS), aby uruchomić aplikację bez debugowania.

Skopiuj adres URL z paska adresu, otwórz inne wystąpienie przeglądarki lub kartę i wklej adres URL na pasku adresu.

Wybierz jedną z przeglądarek, wprowadź nazwę i komunikat, a następnie wybierz przycisk, aby wysłać wiadomość. Nazwa i komunikat są wyświetlane na obu stronach natychmiast:

SignalRBlazor Przykładowa aplikacja otwarta w dwóch oknach przeglądarki przedstawiających wymieniane komunikaty.

Cytaty: Star Trek VI: Nieodkryty kraj ©1991 Paramount

Środowisko hostowane Blazor WebAssembly

Tworzenie aplikacji

Postępuj zgodnie ze wskazówkami dotyczącymi wyboru narzędzi, aby utworzyć hostowaną Blazor WebAssembly aplikację:

Uwaga

Wymagany jest program Visual Studio 2022 lub nowszy oraz zestaw .NET Core SDK 6.0.0 lub nowszy.

Tworzenie nowego projektu.

Blazor WebAssembly Wybierz szablon Aplikacja. Wybierz Dalej.

Wpisz BlazorWebAssemblySignalRApp w polu Nazwa projektu. Upewnij się, że wpis Lokalizacja jest poprawny lub podaj lokalizację projektu. Wybierz Dalej.

W oknie dialogowym Dodatkowe informacje zaznacz pole wyboru ASP.NET Core Hosted.

Wybierz pozycję Utwórz.

Upewnij się, że utworzono hostowaną Blazor WebAssembly aplikację: w Eksplorator rozwiązań potwierdź obecność Client projektu i Server projektu. Jeśli dwa projekty nie są obecne, przed wybraniem pozycji Utwórz rozpocznij od nowa i potwierdź zaznaczenie pola wyboru ASP.NET Core Hosted.

Dodawanie biblioteki SignalR klienta

W Eksplorator rozwiązań kliknij prawym przyciskiem myszy BlazorWebAssemblySignalRApp.Client projekt i wybierz polecenie Zarządzaj pakietami NuGet.

W oknie dialogowym Zarządzanie pakietami NuGet upewnij się, że źródło pakietu ma wartość nuget.org.

Po wybraniu pozycji Przeglądaj wpisz Microsoft.AspNetCore.SignalR.Client w polu wyszukiwania.

W wynikach wyszukiwania wybierz Microsoft.AspNetCore.SignalR.Client pakiet. Ustaw wersję tak, aby odpowiadała współużytkowanej strukturze aplikacji. Wybierz Zainstaluj.

Jeśli zostanie wyświetlone okno dialogowe Podgląd zmian, wybierz przycisk OK.

Jeśli zostanie wyświetlone okno dialogowe Akceptacja licencji, wybierz pozycję Akceptuję, jeśli zgadzasz się z postanowieniami licencyjnymi.

SignalR Dodawanie koncentratora

W projekcie BlazorWebAssemblySignalRApp.Server utwórz folder (mnoga) i dodaj następującą ChatHub klasę Hubs (Hubs/ChatHub.cs):

using Microsoft.AspNetCore.SignalR;

namespace BlazorWebAssemblySignalRApp.Server.Hubs;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
using Microsoft.AspNetCore.SignalR;

namespace BlazorWebAssemblySignalRApp.Server.Hubs;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

namespace BlazorWebAssemblySignalRApp.Server.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

namespace BlazorWebAssemblySignalRApp.Server.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}

Dodawanie usług i punktu końcowego SignalR dla centrum

W projekcie BlazorWebAssemblySignalRApp.Server otwórz Program.cs plik.

Dodaj przestrzeń nazw klasy ChatHub na początku pliku:

using BlazorWebAssemblySignalRApp.Server.Hubs;

Dodaj SignalR i usługi oprogramowania pośredniczącego kompresji odpowiedzi:

builder.Services.AddSignalR();
builder.Services.AddResponseCompression(opts =>
{
      opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
         new[] { "application/octet-stream" });
});

Użyj oprogramowania pośredniczącego kompresji odpowiedzi w górnej części konfiguracji potoku przetwarzania natychmiast po wierszu, który kompiluje aplikację:

app.UseResponseCompression();

Między punktami końcowymi kontrolerów i rezerwowym po stronie klienta dodaj punkt końcowy dla centrum. Natychmiast po wierszu app.MapControllers();dodaj następujący wiersz:

app.MapHub<ChatHub>("/chathub");

W projekcie BlazorWebAssemblySignalRApp.Server otwórz Startup.cs plik.

Dodaj przestrzeń nazw klasy ChatHub na początku pliku:

using BlazorWebAssemblySignalRApp.Server.Hubs;

Dodaj SignalR i usługi oprogramowania pośredniczącego kompresji odpowiedzi:

services.AddSignalR();
services.AddResponseCompression(opts =>
{
      opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
         new[] { "application/octet-stream" });
});

Użyj oprogramowania pośredniczącego kompresji odpowiedzi w górnej części konfiguracji potoku przetwarzania:

app.UseResponseCompression();

Między punktami końcowymi kontrolerów i rezerwowym po stronie klienta dodaj punkt końcowy centrum bezpośrednio po wierszu endpoints.MapControllers();:

endpoints.MapHub<ChatHub>("/chathub");

Dodawanie Razor kodu składnika na potrzeby czatu

W projekcie BlazorWebAssemblySignalRApp.Client otwórz Pages/Index.razor plik.

Zastąp znacznik następującym kodem:

@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager Navigation
@implements IAsyncDisposable

<PageTitle>Index</PageTitle>

<div class="form-group">
    <label>
        User:
        <input @bind="userInput" />
    </label>
</div>
<div class="form-group">
    <label>
        Message:
        <input @bind="messageInput" size="50" />
    </label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private HubConnection? hubConnection;
    private List<string> messages = new List<string>();
    private string? userInput;
    private string? messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            StateHasChanged();
        });

        await hubConnection.StartAsync();
    }

    private async Task Send()
    {
        if (hubConnection is not null)
            {
                await hubConnection.SendAsync("SendMessage", userInput, messageInput);
            }
    }

    public bool IsConnected =>
        hubConnection?.State == HubConnectionState.Connected;

    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
}
@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager Navigation
@implements IAsyncDisposable

<PageTitle>Index</PageTitle>

<div class="form-group">
    <label>
        User:
        <input @bind="userInput" />
    </label>
</div>
<div class="form-group">
    <label>
        Message:
        <input @bind="messageInput" size="50" />
    </label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private HubConnection? hubConnection;
    private List<string> messages = new List<string>();
    private string? userInput;
    private string? messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            StateHasChanged();
        });

        await hubConnection.StartAsync();
    }

    private async Task Send()
    {
        if (hubConnection is not null)
            {
                await hubConnection.SendAsync("SendMessage", userInput, messageInput);
            }
    }

    public bool IsConnected =>
        hubConnection?.State == HubConnectionState.Connected;

    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
}
@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager NavigationManager
@implements IAsyncDisposable

<div class="form-group">
    <label>
        User:
        <input @bind="userInput" />
    </label>
</div>
<div class="form-group">
    <label>
        Message:
        <input @bind="messageInput" size="50" />
    </label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private HubConnection hubConnection;
    private List<string> messages = new List<string>();
    private string userInput;
    private string messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            StateHasChanged();
        });

        await hubConnection.StartAsync();
    }

    async Task Send() =>
        await hubConnection.SendAsync("SendMessage", userInput, messageInput);

    public bool IsConnected =>
        hubConnection.State == HubConnectionState.Connected;

    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
}
@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager NavigationManager
@implements IDisposable

<div class="form-group">
    <label>
        User:
        <input @bind="userInput" />
    </label>
</div>
<div class="form-group">
    <label>
        Message:
        <input @bind="messageInput" size="50" />
    </label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private HubConnection hubConnection;
    private List<string> messages = new List<string>();
    private string userInput;
    private string messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            StateHasChanged();
        });

        await hubConnection.StartAsync();
    }

    async Task Send() =>
        await hubConnection.SendAsync("SendMessage", userInput, messageInput);

    public bool IsConnected =>
        hubConnection.State == HubConnectionState.Connected;

    public void Dispose()
    {
        _ = hubConnection?.DisposeAsync();
    }
}

Uwaga

Wyłącz oprogramowanie pośredniczące kompresji odpowiedzi w Development środowisku podczas korzystania z Przeładowywanie na gorąco. Aby uzyskać więcej informacji, zobacz wskazówki dotyczące platformy ASP.NET CoreBlazorSignalR.

Uruchom aplikację

Postępuj zgodnie ze wskazówkami dotyczącymi narzędzi:

W Eksplorator rozwiązań wybierz BlazorWebAssemblySignalRApp.Server projekt. Naciśnij F5, aby uruchomić aplikację przy użyciu debugowania lub Ctrl+F5 (Windows)/⌘+F5 (macOS), aby uruchomić aplikację bez debugowania.

Ważne

Podczas wykonywania hostowanej Blazor WebAssembly aplikacji uruchom aplikację z projektu rozwiązaniaServer .

Przeglądarka Google Chrome lub Microsoft Edge musi być wybraną przeglądarką na potrzeby sesji debugowania.

Jeśli uruchomienie aplikacji nie powiedzie się w przeglądarce:

  • W konsoli platformy .NET upewnij się, że rozwiązanie jest uruchomione z projektu "Server".
  • Odśwież przeglądarkę przy użyciu przycisku załaduj ponownie przeglądarkę.

Skopiuj adres URL z paska adresu, otwórz inne wystąpienie przeglądarki lub kartę i wklej adres URL na pasku adresu.

Wybierz jedną z przeglądarek, wprowadź nazwę i komunikat, a następnie wybierz przycisk, aby wysłać wiadomość. Nazwa i komunikat są wyświetlane na obu stronach natychmiast:

SignalRBlazor Przykładowa aplikacja otwarta w dwóch oknach przeglądarki przedstawiających wymieniane komunikaty.

Cytaty: Star Trek VI: Nieodkryty kraj ©1991 Paramount

Następne kroki

W tym samouczku zawarto informacje na temat wykonywania następujących czynności:

  • Tworzenie Blazor aplikacji
  • Dodawanie biblioteki SignalR klienta
  • SignalR Dodawanie koncentratora
  • Dodawanie SignalR usług i punktu końcowego SignalR dla centrum
  • Razor Dodawanie kodu składnika na potrzeby czatu

Aby uzyskać szczegółowe wskazówki dotyczące platform SignalR i Blazor , zobacz następujące zestawy dokumentacji referencyjnej:

Dodatkowe zasoby