mirror of
https://github.com/meysamhadeli/booking-microservices.git
synced 2026-04-24 14:28:09 +08:00
.
This commit is contained in:
parent
339315f75b
commit
e62d9ea815
25
src/Services/Booking/src/Booking/Protos/foo.proto
Normal file
25
src/Services/Booking/src/Booking/Protos/foo.proto
Normal file
@ -0,0 +1,25 @@
|
||||
syntax = "proto3";
|
||||
option csharp_namespace = "GrpcSamples";
|
||||
|
||||
service FooService {
|
||||
rpc GetFoo (FooRequest) returns (FooResponse);
|
||||
|
||||
rpc GetFoos(FooServerStreamingRequest) returns (stream FooResponse);
|
||||
|
||||
rpc SendFoos(stream FooRequest) returns (FooResponse);
|
||||
|
||||
rpc SendAndGetFoos(stream FooRequest) returns (stream FooResponse);
|
||||
}
|
||||
|
||||
message FooRequest {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
message FooServerStreamingRequest {
|
||||
string message = 1;
|
||||
int32 messageCount = 2;
|
||||
}
|
||||
|
||||
message FooResponse {
|
||||
string message = 1;
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using BuildingBlocks.MassTransit;
|
||||
using BuildingBlocks.Web;
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NSubstitute;
|
||||
using Respawn;
|
||||
|
||||
namespace Integration.Test;
|
||||
|
||||
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
public Checkpoint Checkpoint { get; set; }
|
||||
public IConfiguration Configuration => Services.GetRequiredService<IConfiguration>();
|
||||
public Action<IServiceCollection>? TestRegistrationServices { get; set; }
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
//https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests#set-the-environment
|
||||
//https://stackoverflow.com/questions/43927955/should-getenvironmentvariable-work-in-xunit-test/43951218
|
||||
|
||||
//we could read env from our test launch setting or we can set it directly here
|
||||
builder.UseEnvironment("test");
|
||||
|
||||
//The test app's builder.ConfigureTestServices callback is executed after the app's Startup.ConfigureServices code is executed.
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll(typeof(IHostedService));
|
||||
services.ReplaceSingleton(AddHttpContextAccessorMock);
|
||||
TestRegistrationServices?.Invoke(services);
|
||||
services.AddMassTransitTestHarness(x =>
|
||||
{
|
||||
x.UsingRabbitMq((context, cfg) =>
|
||||
{
|
||||
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>("RabbitMq");
|
||||
var host = rabbitMqOptions.HostName;
|
||||
|
||||
cfg.Host(host, h =>
|
||||
{
|
||||
h.Username(rabbitMqOptions.UserName);
|
||||
h.Password(rabbitMqOptions.Password);
|
||||
});
|
||||
cfg.ConfigureEndpoints(context);
|
||||
});
|
||||
});
|
||||
|
||||
Checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}};
|
||||
|
||||
TestRegistrationServices?.Invoke(services);
|
||||
});
|
||||
|
||||
builder.UseDefaultServiceProvider((env, c) =>
|
||||
{
|
||||
// Handling Captive Dependency Problem
|
||||
// https://ankitvijay.net/2020/03/17/net-core-and-di-beware-of-captive-dependency/
|
||||
// https://blog.ploeh.dk/2014/06/02/captive-dependency/
|
||||
if (env.HostingEnvironment.IsEnvironment("test") || env.HostingEnvironment.IsDevelopment())
|
||||
c.ValidateScopes = true;
|
||||
});
|
||||
}
|
||||
|
||||
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
|
||||
{
|
||||
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider};
|
||||
|
||||
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012);
|
||||
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
|
||||
|
||||
return httpContextAccessorMock;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Booking.Api\Booking.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Booking.Data;
|
||||
using BuildingBlocks.Domain.Model;
|
||||
using BuildingBlocks.EFCore;
|
||||
using Grpc.Net.Client;
|
||||
using MassTransit.Testing;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using Serilog;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Integration.Test;
|
||||
|
||||
[CollectionDefinition(nameof(IntegrationTestFixture))]
|
||||
public class SliceFixtureCollection : ICollectionFixture<IntegrationTestFixture> { }
|
||||
|
||||
public class IntegrationTestFixture : IAsyncLifetime
|
||||
{
|
||||
private readonly CustomWebApplicationFactory _factory;
|
||||
|
||||
public IntegrationTestFixture()
|
||||
{
|
||||
// Ref: https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0#basic-tests-with-the-default-webapplicationfactory
|
||||
_factory = new CustomWebApplicationFactory();
|
||||
}
|
||||
|
||||
public IServiceProvider ServiceProvider => _factory.Services;
|
||||
public IConfiguration Configuration => _factory.Configuration;
|
||||
public HttpClient HttpClient => _factory.CreateClient();
|
||||
public ITestHarness TestHarness => CreateHarness();
|
||||
public GrpcChannel Channel => CreateChannel();
|
||||
|
||||
// ref: https://github.com/trbenning/serilog-sinks-xunit
|
||||
public ILogger CreateLogger(ITestOutputHelper output)
|
||||
{
|
||||
if (output != null)
|
||||
{
|
||||
return new LoggerConfiguration()
|
||||
.WriteTo.TestOutput(output)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RegisterTestServices(Action<IServiceCollection> services) => _factory.TestRegistrationServices = services;
|
||||
|
||||
public virtual Task InitializeAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual async Task DisposeAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Configuration?.GetConnectionString("DefaultConnection")))
|
||||
await _factory.Checkpoint.Reset(Configuration?.GetConnectionString("DefaultConnection"));
|
||||
|
||||
await _factory.DisposeAsync();
|
||||
}
|
||||
|
||||
public async Task ExecuteScopeAsync(Func<IServiceProvider, Task> action)
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
await action(scope.ServiceProvider);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteScopeAsync<T>(Func<IServiceProvider, Task<T>> action)
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
|
||||
var result = await action(scope.ServiceProvider);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task ExecuteDbContextAsync(Func<BookingDbContext, Task> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>()));
|
||||
}
|
||||
|
||||
public Task ExecuteDbContextAsync(Func<BookingDbContext, ValueTask> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>()).AsTask());
|
||||
}
|
||||
|
||||
public Task ExecuteDbContextAsync(Func<BookingDbContext, IMediator, Task> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>(), sp.GetService<IMediator>()));
|
||||
}
|
||||
|
||||
public Task<T> ExecuteDbContextAsync<T>(Func<BookingDbContext, Task<T>> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>()));
|
||||
}
|
||||
|
||||
public Task<T> ExecuteDbContextAsync<T>(Func<BookingDbContext, ValueTask<T>> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>()).AsTask());
|
||||
}
|
||||
|
||||
public Task<T> ExecuteDbContextAsync<T>(Func<BookingDbContext, IMediator, Task<T>> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>(), sp.GetService<IMediator>()));
|
||||
}
|
||||
|
||||
public Task InsertAsync<T>(params T[] entities) where T : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
foreach (var entity in entities) db.Set<T>().Add(entity);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity>(TEntity entity) where TEntity : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity, TEntity2>(TEntity entity, TEntity2 entity2)
|
||||
where TEntity : class
|
||||
where TEntity2 : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
db.Set<TEntity2>().Add(entity2);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity, TEntity2, TEntity3>(TEntity entity, TEntity2 entity2, TEntity3 entity3)
|
||||
where TEntity : class
|
||||
where TEntity2 : class
|
||||
where TEntity3 : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
db.Set<TEntity2>().Add(entity2);
|
||||
db.Set<TEntity3>().Add(entity3);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>(TEntity entity, TEntity2 entity2, TEntity3 entity3,
|
||||
TEntity4 entity4)
|
||||
where TEntity : class
|
||||
where TEntity2 : class
|
||||
where TEntity3 : class
|
||||
where TEntity4 : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
db.Set<TEntity2>().Add(entity2);
|
||||
db.Set<TEntity3>().Add(entity3);
|
||||
db.Set<TEntity4>().Add(entity4);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task<T> FindAsync<T>(long id)
|
||||
where T : class, IEntity
|
||||
{
|
||||
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
|
||||
}
|
||||
|
||||
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
|
||||
{
|
||||
return ExecuteScopeAsync(sp =>
|
||||
{
|
||||
var mediator = sp.GetRequiredService<IMediator>();
|
||||
|
||||
return mediator.Send(request);
|
||||
});
|
||||
}
|
||||
|
||||
public Task SendAsync(IRequest request)
|
||||
{
|
||||
return ExecuteScopeAsync(sp =>
|
||||
{
|
||||
var mediator = sp.GetRequiredService<IMediator>();
|
||||
|
||||
return mediator.Send(request);
|
||||
});
|
||||
}
|
||||
|
||||
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
|
||||
{
|
||||
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider};
|
||||
|
||||
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 5000);
|
||||
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
|
||||
|
||||
return httpContextAccessorMock;
|
||||
}
|
||||
|
||||
private ITestHarness CreateHarness()
|
||||
{
|
||||
var harness = ServiceProvider.GetTestHarness();
|
||||
harness.Start().GetAwaiter().GetResult();
|
||||
return harness;
|
||||
}
|
||||
|
||||
private GrpcChannel CreateChannel()
|
||||
{
|
||||
return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient});
|
||||
}
|
||||
|
||||
private async Task EnsureDatabaseAsync()
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
|
||||
var context = scope.ServiceProvider.GetRequiredService<BookingDbContext>();
|
||||
var seeders = scope.ServiceProvider.GetServices<IDataSeeder>();
|
||||
|
||||
await context.Database.MigrateAsync();
|
||||
|
||||
foreach (var seeder in seeders) await seeder.SeedAllAsync();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using BuildingBlocks.MassTransit;
|
||||
using BuildingBlocks.Web;
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NSubstitute;
|
||||
using Respawn;
|
||||
|
||||
namespace Integration.Test;
|
||||
|
||||
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
public Checkpoint Checkpoint { get; set; }
|
||||
public IConfiguration Configuration => Services.GetRequiredService<IConfiguration>();
|
||||
public Action<IServiceCollection>? TestRegistrationServices { get; set; }
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
//https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests#set-the-environment
|
||||
//https://stackoverflow.com/questions/43927955/should-getenvironmentvariable-work-in-xunit-test/43951218
|
||||
|
||||
//we could read env from our test launch setting or we can set it directly here
|
||||
builder.UseEnvironment("test");
|
||||
|
||||
//The test app's builder.ConfigureTestServices callback is executed after the app's Startup.ConfigureServices code is executed.
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll(typeof(IHostedService));
|
||||
services.ReplaceSingleton(AddHttpContextAccessorMock);
|
||||
TestRegistrationServices?.Invoke(services);
|
||||
services.AddMassTransitTestHarness(x =>
|
||||
{
|
||||
x.UsingRabbitMq((context, cfg) =>
|
||||
{
|
||||
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>("RabbitMq");
|
||||
var host = rabbitMqOptions.HostName;
|
||||
|
||||
cfg.Host(host, h =>
|
||||
{
|
||||
h.Username(rabbitMqOptions.UserName);
|
||||
h.Password(rabbitMqOptions.Password);
|
||||
});
|
||||
cfg.ConfigureEndpoints(context);
|
||||
});
|
||||
});
|
||||
|
||||
Checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}};
|
||||
|
||||
TestRegistrationServices?.Invoke(services);
|
||||
});
|
||||
|
||||
builder.UseDefaultServiceProvider((env, c) =>
|
||||
{
|
||||
// Handling Captive Dependency Problem
|
||||
// https://ankitvijay.net/2020/03/17/net-core-and-di-beware-of-captive-dependency/
|
||||
// https://blog.ploeh.dk/2014/06/02/captive-dependency/
|
||||
if (env.HostingEnvironment.IsEnvironment("test") || env.HostingEnvironment.IsDevelopment())
|
||||
c.ValidateScopes = true;
|
||||
});
|
||||
}
|
||||
|
||||
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
|
||||
{
|
||||
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider};
|
||||
|
||||
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012);
|
||||
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
|
||||
|
||||
return httpContextAccessorMock;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BuildingBlocks.Domain.Model;
|
||||
using BuildingBlocks.EFCore;
|
||||
using Flight.Data;
|
||||
using Grpc.Net.Client;
|
||||
using MassTransit.Testing;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using Serilog;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Integration.Test;
|
||||
|
||||
[CollectionDefinition(nameof(IntegrationTestFixture))]
|
||||
public class SliceFixtureCollection : ICollectionFixture<IntegrationTestFixture> { }
|
||||
|
||||
public class IntegrationTestFixture : IAsyncLifetime
|
||||
{
|
||||
private readonly CustomWebApplicationFactory _factory;
|
||||
|
||||
public IntegrationTestFixture()
|
||||
{
|
||||
// Ref: https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0#basic-tests-with-the-default-webapplicationfactory
|
||||
_factory = new CustomWebApplicationFactory();
|
||||
}
|
||||
|
||||
public IServiceProvider ServiceProvider => _factory.Services;
|
||||
public IConfiguration Configuration => _factory.Configuration;
|
||||
public HttpClient HttpClient => _factory.CreateClient();
|
||||
public ITestHarness TestHarness => CreateHarness();
|
||||
public GrpcChannel Channel => CreateChannel();
|
||||
|
||||
// ref: https://github.com/trbenning/serilog-sinks-xunit
|
||||
public ILogger CreateLogger(ITestOutputHelper output)
|
||||
{
|
||||
if (output != null)
|
||||
{
|
||||
return new LoggerConfiguration()
|
||||
.WriteTo.TestOutput(output)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RegisterTestServices(Action<IServiceCollection> services) => _factory.TestRegistrationServices = services;
|
||||
|
||||
public virtual Task InitializeAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual async Task DisposeAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Configuration?.GetConnectionString("DefaultConnection")))
|
||||
await _factory.Checkpoint.Reset(Configuration?.GetConnectionString("DefaultConnection"));
|
||||
|
||||
await _factory.DisposeAsync();
|
||||
}
|
||||
|
||||
public async Task ExecuteScopeAsync(Func<IServiceProvider, Task> action)
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
await action(scope.ServiceProvider);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteScopeAsync<T>(Func<IServiceProvider, Task<T>> action)
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
|
||||
var result = await action(scope.ServiceProvider);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task ExecuteDbContextAsync(Func<FlightDbContext, Task> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>()));
|
||||
}
|
||||
|
||||
public Task ExecuteDbContextAsync(Func<FlightDbContext, ValueTask> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>()).AsTask());
|
||||
}
|
||||
|
||||
public Task ExecuteDbContextAsync(Func<FlightDbContext, IMediator, Task> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>(), sp.GetService<IMediator>()));
|
||||
}
|
||||
|
||||
public Task<T> ExecuteDbContextAsync<T>(Func<FlightDbContext, Task<T>> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>()));
|
||||
}
|
||||
|
||||
public Task<T> ExecuteDbContextAsync<T>(Func<FlightDbContext, ValueTask<T>> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>()).AsTask());
|
||||
}
|
||||
|
||||
public Task<T> ExecuteDbContextAsync<T>(Func<FlightDbContext, IMediator, Task<T>> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>(), sp.GetService<IMediator>()));
|
||||
}
|
||||
|
||||
public Task InsertAsync<T>(params T[] entities) where T : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
foreach (var entity in entities) db.Set<T>().Add(entity);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity>(TEntity entity) where TEntity : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity, TEntity2>(TEntity entity, TEntity2 entity2)
|
||||
where TEntity : class
|
||||
where TEntity2 : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
db.Set<TEntity2>().Add(entity2);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity, TEntity2, TEntity3>(TEntity entity, TEntity2 entity2, TEntity3 entity3)
|
||||
where TEntity : class
|
||||
where TEntity2 : class
|
||||
where TEntity3 : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
db.Set<TEntity2>().Add(entity2);
|
||||
db.Set<TEntity3>().Add(entity3);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>(TEntity entity, TEntity2 entity2, TEntity3 entity3,
|
||||
TEntity4 entity4)
|
||||
where TEntity : class
|
||||
where TEntity2 : class
|
||||
where TEntity3 : class
|
||||
where TEntity4 : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
db.Set<TEntity2>().Add(entity2);
|
||||
db.Set<TEntity3>().Add(entity3);
|
||||
db.Set<TEntity4>().Add(entity4);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task<T> FindAsync<T>(long id)
|
||||
where T : class, IEntity
|
||||
{
|
||||
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
|
||||
}
|
||||
|
||||
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
|
||||
{
|
||||
return ExecuteScopeAsync(sp =>
|
||||
{
|
||||
var mediator = sp.GetRequiredService<IMediator>();
|
||||
|
||||
return mediator.Send(request);
|
||||
});
|
||||
}
|
||||
|
||||
public Task SendAsync(IRequest request)
|
||||
{
|
||||
return ExecuteScopeAsync(sp =>
|
||||
{
|
||||
var mediator = sp.GetRequiredService<IMediator>();
|
||||
|
||||
return mediator.Send(request);
|
||||
});
|
||||
}
|
||||
|
||||
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
|
||||
{
|
||||
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider};
|
||||
|
||||
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 5000);
|
||||
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
|
||||
|
||||
return httpContextAccessorMock;
|
||||
}
|
||||
|
||||
private ITestHarness CreateHarness()
|
||||
{
|
||||
var harness = ServiceProvider.GetTestHarness();
|
||||
harness.Start().GetAwaiter().GetResult();
|
||||
return harness;
|
||||
}
|
||||
|
||||
private GrpcChannel CreateChannel()
|
||||
{
|
||||
return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient});
|
||||
}
|
||||
|
||||
private async Task EnsureDatabaseAsync()
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
|
||||
var context = scope.ServiceProvider.GetRequiredService<FlightDbContext>();
|
||||
var seeders = scope.ServiceProvider.GetServices<IDataSeeder>();
|
||||
|
||||
await context.Database.MigrateAsync();
|
||||
|
||||
foreach (var seeder in seeders) await seeder.SeedAllAsync();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using BuildingBlocks.MassTransit;
|
||||
using BuildingBlocks.Web;
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NSubstitute;
|
||||
using Respawn;
|
||||
|
||||
namespace Integration.Test;
|
||||
|
||||
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
public Checkpoint Checkpoint { get; set; }
|
||||
public IConfiguration Configuration => Services.GetRequiredService<IConfiguration>();
|
||||
public Action<IServiceCollection>? TestRegistrationServices { get; set; }
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
//https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests#set-the-environment
|
||||
//https://stackoverflow.com/questions/43927955/should-getenvironmentvariable-work-in-xunit-test/43951218
|
||||
|
||||
//we could read env from our test launch setting or we can set it directly here
|
||||
builder.UseEnvironment("test");
|
||||
|
||||
//The test app's builder.ConfigureTestServices callback is executed after the app's Startup.ConfigureServices code is executed.
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll(typeof(IHostedService));
|
||||
services.ReplaceSingleton(AddHttpContextAccessorMock);
|
||||
TestRegistrationServices?.Invoke(services);
|
||||
services.AddMassTransitTestHarness(x =>
|
||||
{
|
||||
x.UsingRabbitMq((context, cfg) =>
|
||||
{
|
||||
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>("RabbitMq");
|
||||
var host = rabbitMqOptions.HostName;
|
||||
|
||||
cfg.Host(host, h =>
|
||||
{
|
||||
h.Username(rabbitMqOptions.UserName);
|
||||
h.Password(rabbitMqOptions.Password);
|
||||
});
|
||||
cfg.ConfigureEndpoints(context);
|
||||
});
|
||||
});
|
||||
|
||||
Checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}};
|
||||
|
||||
TestRegistrationServices?.Invoke(services);
|
||||
});
|
||||
|
||||
builder.UseDefaultServiceProvider((env, c) =>
|
||||
{
|
||||
// Handling Captive Dependency Problem
|
||||
// https://ankitvijay.net/2020/03/17/net-core-and-di-beware-of-captive-dependency/
|
||||
// https://blog.ploeh.dk/2014/06/02/captive-dependency/
|
||||
if (env.HostingEnvironment.IsEnvironment("test") || env.HostingEnvironment.IsDevelopment())
|
||||
c.ValidateScopes = true;
|
||||
});
|
||||
}
|
||||
|
||||
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
|
||||
{
|
||||
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider};
|
||||
|
||||
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012);
|
||||
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
|
||||
|
||||
return httpContextAccessorMock;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BuildingBlocks.Domain.Model;
|
||||
using BuildingBlocks.EFCore;
|
||||
using Grpc.Net.Client;
|
||||
using Identity.Data;
|
||||
using MassTransit.Testing;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using Serilog;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Integration.Test;
|
||||
|
||||
[CollectionDefinition(nameof(IntegrationTestFixture))]
|
||||
public class SliceFixtureCollection : ICollectionFixture<IntegrationTestFixture> { }
|
||||
|
||||
public class IntegrationTestFixture : IAsyncLifetime
|
||||
{
|
||||
private readonly CustomWebApplicationFactory _factory;
|
||||
|
||||
public IntegrationTestFixture()
|
||||
{
|
||||
// Ref: https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0#basic-tests-with-the-default-webapplicationfactory
|
||||
_factory = new CustomWebApplicationFactory();
|
||||
}
|
||||
|
||||
public IServiceProvider ServiceProvider => _factory.Services;
|
||||
public IConfiguration Configuration => _factory.Configuration;
|
||||
public HttpClient HttpClient => _factory.CreateClient();
|
||||
public ITestHarness TestHarness => CreateHarness();
|
||||
public GrpcChannel Channel => CreateChannel();
|
||||
|
||||
// ref: https://github.com/trbenning/serilog-sinks-xunit
|
||||
public ILogger CreateLogger(ITestOutputHelper output)
|
||||
{
|
||||
if (output != null)
|
||||
{
|
||||
return new LoggerConfiguration()
|
||||
.WriteTo.TestOutput(output)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RegisterTestServices(Action<IServiceCollection> services) => _factory.TestRegistrationServices = services;
|
||||
|
||||
public virtual Task InitializeAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual async Task DisposeAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Configuration?.GetConnectionString("DefaultConnection")))
|
||||
await _factory.Checkpoint.Reset(Configuration?.GetConnectionString("DefaultConnection"));
|
||||
|
||||
await _factory.DisposeAsync();
|
||||
}
|
||||
|
||||
public async Task ExecuteScopeAsync(Func<IServiceProvider, Task> action)
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
await action(scope.ServiceProvider);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteScopeAsync<T>(Func<IServiceProvider, Task<T>> action)
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
|
||||
var result = await action(scope.ServiceProvider);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task ExecuteDbContextAsync(Func<IdentityContext, Task> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>()));
|
||||
}
|
||||
|
||||
public Task ExecuteDbContextAsync(Func<IdentityContext, ValueTask> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>()).AsTask());
|
||||
}
|
||||
|
||||
public Task ExecuteDbContextAsync(Func<IdentityContext, IMediator, Task> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>(), sp.GetService<IMediator>()));
|
||||
}
|
||||
|
||||
public Task<T> ExecuteDbContextAsync<T>(Func<IdentityContext, Task<T>> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>()));
|
||||
}
|
||||
|
||||
public Task<T> ExecuteDbContextAsync<T>(Func<IdentityContext, ValueTask<T>> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>()).AsTask());
|
||||
}
|
||||
|
||||
public Task<T> ExecuteDbContextAsync<T>(Func<IdentityContext, IMediator, Task<T>> action)
|
||||
{
|
||||
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>(), sp.GetService<IMediator>()));
|
||||
}
|
||||
|
||||
public Task InsertAsync<T>(params T[] entities) where T : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
foreach (var entity in entities) db.Set<T>().Add(entity);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity>(TEntity entity) where TEntity : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity, TEntity2>(TEntity entity, TEntity2 entity2)
|
||||
where TEntity : class
|
||||
where TEntity2 : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
db.Set<TEntity2>().Add(entity2);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity, TEntity2, TEntity3>(TEntity entity, TEntity2 entity2, TEntity3 entity3)
|
||||
where TEntity : class
|
||||
where TEntity2 : class
|
||||
where TEntity3 : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
db.Set<TEntity2>().Add(entity2);
|
||||
db.Set<TEntity3>().Add(entity3);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>(TEntity entity, TEntity2 entity2, TEntity3 entity3,
|
||||
TEntity4 entity4)
|
||||
where TEntity : class
|
||||
where TEntity2 : class
|
||||
where TEntity3 : class
|
||||
where TEntity4 : class
|
||||
{
|
||||
return ExecuteDbContextAsync(db =>
|
||||
{
|
||||
db.Set<TEntity>().Add(entity);
|
||||
db.Set<TEntity2>().Add(entity2);
|
||||
db.Set<TEntity3>().Add(entity3);
|
||||
db.Set<TEntity4>().Add(entity4);
|
||||
|
||||
return db.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public Task<T> FindAsync<T>(long id)
|
||||
where T : class, IEntity
|
||||
{
|
||||
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
|
||||
}
|
||||
|
||||
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
|
||||
{
|
||||
return ExecuteScopeAsync(sp =>
|
||||
{
|
||||
var mediator = sp.GetRequiredService<IMediator>();
|
||||
|
||||
return mediator.Send(request);
|
||||
});
|
||||
}
|
||||
|
||||
public Task SendAsync(IRequest request)
|
||||
{
|
||||
return ExecuteScopeAsync(sp =>
|
||||
{
|
||||
var mediator = sp.GetRequiredService<IMediator>();
|
||||
|
||||
return mediator.Send(request);
|
||||
});
|
||||
}
|
||||
|
||||
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
|
||||
{
|
||||
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider};
|
||||
|
||||
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 5000);
|
||||
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
|
||||
|
||||
return httpContextAccessorMock;
|
||||
}
|
||||
|
||||
private ITestHarness CreateHarness()
|
||||
{
|
||||
var harness = ServiceProvider.GetTestHarness();
|
||||
harness.Start().GetAwaiter().GetResult();
|
||||
return harness;
|
||||
}
|
||||
|
||||
private GrpcChannel CreateChannel()
|
||||
{
|
||||
return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient});
|
||||
}
|
||||
|
||||
private async Task EnsureDatabaseAsync()
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
|
||||
var context = scope.ServiceProvider.GetRequiredService<IdentityContext>();
|
||||
var seeders = scope.ServiceProvider.GetServices<IDataSeeder>();
|
||||
|
||||
await context.Database.MigrateAsync();
|
||||
|
||||
foreach (var seeder in seeders) await seeder.SeedAllAsync();
|
||||
}
|
||||
}
|
||||
32
src/Services/Passenger/src/Passenger/Protos/test.proto
Normal file
32
src/Services/Passenger/src/Passenger/Protos/test.proto
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright 2019 The gRPC Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package test;
|
||||
|
||||
service Tester {
|
||||
rpc SayHelloUnary (HelloRequest) returns (HelloReply);
|
||||
rpc SayHelloServerStreaming (HelloRequest) returns (stream HelloReply);
|
||||
rpc SayHelloClientStreaming (stream HelloRequest) returns (HelloReply);
|
||||
rpc SayHelloBidirectionalStreaming (stream HelloRequest) returns (stream HelloReply);
|
||||
}
|
||||
|
||||
message HelloRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message HelloReply {
|
||||
string message = 1;
|
||||
}
|
||||
38
src/Services/Passenger/src/Passenger/Services/Greeter.cs
Normal file
38
src/Services/Passenger/src/Passenger/Services/Greeter.cs
Normal file
@ -0,0 +1,38 @@
|
||||
#region Copyright notice and license
|
||||
|
||||
// Copyright 2019 The gRPC Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#endregion
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
public class Greeter : IGreeter
|
||||
{
|
||||
private readonly ILogger<Greeter> _logger;
|
||||
|
||||
public Greeter(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<Greeter>();
|
||||
}
|
||||
|
||||
public string Greet(string name)
|
||||
{
|
||||
_logger.LogInformation($"Creating greeting to {name}");
|
||||
return $"Hello {name}";
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Services/Passenger/src/Passenger/Services/IGreeter.cs
Normal file
25
src/Services/Passenger/src/Passenger/Services/IGreeter.cs
Normal file
@ -0,0 +1,25 @@
|
||||
#region Copyright notice and license
|
||||
|
||||
// Copyright 2019 The gRPC Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#endregion
|
||||
|
||||
namespace Server
|
||||
{
|
||||
public interface IGreeter
|
||||
{
|
||||
string Greet(string name);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user