Een Newsletter- Subscription-Service-Teil 2 - De Diensten herfactoren | ( | en een beetje hangvuur |)
Inleiding
In deel 1 van deze reeks. Ik heb jullie laten zien hoe ik een nieuwe Newsletter Subscription-pagina creëerde. In dit deel zal ik behandelen hoe ik de oplossing herstructureerde om het delen van Diensten en Modellen tussen het Webproject (MostlylucidM SK3 en het Scheduler-Service-project ♫(MostslylucideMSC5SchedulerService ).
De projecten
Oorspronkelijk had ik slechts een enkel, monolithisch project dat alle code voor de website bevat. Dit is een redelijke aanpak voor kleinere toepassingen , het geeft je een eenvoudige manier om te navigeren Ook het uitdelen van de schedulerdienst is logisch voor mij, want ik kan dit als een aparte dockercontainer opzetten waardoor ik de website kan updaten zonder dat de scheduler opnieuw begint.
Om dit te doen, heb ik de bezorgdheid logisch gegroepeerd in 5projecten. Dit is een gemeenschappelijke aanpak in ASPM SK2NET-applicaties .
Ik kan nu testprojecten toevoegen voor elk van deze en ze isoleerlijk testen.
Meestlucid
Dit project is een ASP.NET Core M SK1 webproject, het bevat alle Controllers & Views om pagina's aan de gebruiker te tonen
Meestlucid.DbContext
Hierin staat mijn belangrijkste context-definitie voor interactie met de database. Inbegrepen EF Core DbContext.
Meestlucid.Services
Dit is het belangrijkste bibliotheekproject dat alle dienste bevat die met de database interageren.
Meestlucid.Gedeeld
Dit is een klas bibliotheekproject dat alle gedeelde modellen bevat die gebruikt worden door zowel de Mostlylucid en Mostly Lucid.SchedulerService projecten.
Meestlucid.SchedulerService
Dit is een webapplicatie die de Hangfire-dienst beheert, die de gescheduleerde taken uitvoert en ook de eindpunten om e-mails te sturen.
De structuur hiervan is onderaan te zien. Je kunt zien dat SchedulerService en het hoofd Mostlylucid alleen de DbContext klas gebruiken voor de eerste opstelling.
graph LR Mostlylucid[Mostlylucid] --> |Intialize| Mostlylucid_DbContext[Mostlylucid.DbContext] Mostlylucid[Mostlylucid] --> Mostlylucid_Shared[Mostlylucid.Shared] Mostlylucid[Mostlylucid] --> Mostlylucid_Services[Mostlylucid.Services] Mostlylucid_DbContext[Mostlylucid.DbContext] --> Mostlylucid_Shared[Mostlylucid.Shared] Mostlylucid_Services[Mostlylucid.Services] --> Mostlylucid_DbContext[Mostlylucid.DbContext] Mostlylucid_Services[Mostlylucid.Services] --> Mostlylucid_Shared[Mostlylucid.Shared] Mostlylucid_SchedulerService[Mostlylucid.SchedulerService] --> Mostlylucid_Shared[Mostlylucid.Shared] Mostlylucid_SchedulerService[Mostlylucid.SchedulerService] --> |Intialize|Mostlylucid_DbContext[Mostlylucid.DbContext] Mostlylucid_SchedulerService[Mostlylucid.SchedulerService] --> Mostlylucid_Services[Mostlylucid.Services]
Zoals gewoonlijk gebruik ik een uitbreidingsmethode als ingangspunt voor het opzetten van de database.
public static class Setup
{
public static void SetupDatabase(this IServiceCollection services, IConfiguration configuration,
IWebHostEnvironment env, string applicationName="mostlylucid")
{
services.AddDbContext<IMostlylucidDBContext, MostlylucidDbContext>(options =>
{
if (env.IsDevelopment())
{
options.EnableDetailedErrors(true);
options.EnableSensitiveDataLogging(true);
}
var connectionString = configuration.GetConnectionString("DefaultConnection");
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = applicationName
};
options.UseNpgsql(connectionStringBuilder.ConnectionString);
});
}
}
Behalve het roepen van deze methode heeft geen enkel project op topniveau enige afhankelijkheden van het DbContext project.
NOTA: Het is belangrijk om een raceconditie te vermijden bij het inisialiseren van DbContext ( vooral als je migraties uitvoert ) om dit simpelweg hier te doen Ik voegde net een afhankelijkheid toe in mijn Docker Compose-lêer om ervoor te zorgen dat het belangrijkste Mostlylucid project opgaat en loopt voordat ik mijn SchedulerService begin.
depends_on:
- mostlylucid
healthcheck:
test: ["CMD", "curl", "-f", "http://mostlylucid:80/healthy"]
interval: 30s
timeout: 10s
retries: 5
Je kunt dit ook op andere manieren doen., bijvoorbeeld in het verleden wanneer je meerdere voorbeelden van dezelfde app uitvoert die ik gebruikt heb. Redis om een sluitvlag te zetten die elke instance controleert voordat migraties worden uitgevoerd / andere taken die ervoor zorgen dat slechts één instance tegelijk kan worden uitgevoerd
Natuurlijk heeft Redis ook veel andere toepassingen; vooral met ASPM SK1NET 8's nieuwe IDistributedCache interface waarmee je Redis als cache-provider kunt gebruiken met een tag based expiration.
Hangfire-opstelling
Ik besloot Hangfire te gebruiken om mijn scheduling te beheren, want het heeft een handige integratie met ASP.NET Core en is gemakkelijk te gebruiken.
Hiervoor heb ik in het SchedulerService project de volgende NuGet-pakketten toegevoegd.
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.PostgreSql
Dit betekent dat ik Postgres als mijn winkel kan gebruiken voor de gescheduleerde taken.
Nu heb ik Hangfire seetup Ik kan mijn gescheduleerde taken toevoegen.
public static class JobInitializer
{
private const string AutoNewsletterJob = "AutoNewsletterJob";
private const string DailyNewsletterJob = "DailyNewsletterJob";
private const string WeeklyNewsletterJob = "WeeklyNewsletterJob";
private const string MonthlyNewsletterJob = "MonthlyNewsletterJob";
public static void InitializeJobs(this IApplicationBuilder app)
{
var scope= app.ApplicationServices.CreateScope();
var recurringJobManager = scope.ServiceProvider.GetRequiredService<RecurringJobManager>();
recurringJobManager.AddOrUpdate<NewsletterSendingService>(AutoNewsletterJob, x => x.SendNewsletter(SubscriptionType.EveryPost), Cron.Hourly);
recurringJobManager.AddOrUpdate<NewsletterSendingService>(DailyNewsletterJob, x => x.SendNewsletter(SubscriptionType.Daily), "0 17 * * *");
recurringJobManager.AddOrUpdate<NewsletterSendingService>(WeeklyNewsletterJob, x => x.SendNewsletter(SubscriptionType.Weekly), "0 17 * * *");
recurringJobManager.AddOrUpdate<NewsletterSendingService>(MonthlyNewsletterJob, x => x.SendNewsletter(SubscriptionType.Monthly), "0 17 * * *");
}
}
Hier zie je dat ik een baan voor elk heb. SubscriptionType Het is een vlag die bepaalt hoe vaak de newsletter wordt gestuurd. Cron class om de frequentie van het werk te bepalen.
Omdat ik de dag kan kiezen voor zowel maandelijkse als weekelijke abonnementen, stel ik de tijd op 17:00 (5pm) want dit is een goede tijd om newsletters te sturen ( het einde van de dag in het Verenigd Koninkrijk en te beginnen in de Verenigde Staten Ik heb ook een baan die elke uur draait om de newsletter te sturen naar de leden van elke post.
Dit roept dan op tot een SendNewsletter de methode in het NewsletterSendingService class. Waar ik later meer in detail zal gaan.
Dtos en Mapping
Ik gebruik DTO's om gegevens tussen de lagen van mijn applicatie te vervoeren. Dit voegt complexiteit toe omdat ik ( vaak twee keer moet tonen ) de Inskrywings naar Dtos en dan de Dtos naar ViewModels moet toekennen ♫( en terugM SK4 Ik vind deze scheiding van zorgen wel waardevol, want het laat me de onderliggende datastructuur veranderen zonder de UI te beïnvloeden.
Je kunt gebruik maken van benaderingen zoals AutoMapper / Mapster etc. om deze mapping te doen. De gebruik van deze kan ook significante prestatie voordelen hebben voor ♫.AsNoTrackingM SK3 vragen, omdat je direct naar de Dto kunt mappen en de overhead van tracking-wijzigingen kan vermijdenMSC4 Automapper heeft bijvoorbeeld een Uitbreiding die kan gevraagd worden methode waarmee je direct naar de Dto kunt in kaart brengen in de zoekopdracht.
Maar in dit geval besloot ik alleen maar mapperverlengingen toe te voegen waar ik het nodig had. Dit geeft me meer controle over de mapping voor elk niveau ( maar is meer werk , vooral als je een heleboel entiteiten hebt
public static class BlogPostEntityMapper
{
public static BlogPostDto ToDto(this BlogPostEntity entity, string[] languages = null)
{
return new BlogPostDto
{
Id = entity.Id.ToString(),
Title = entity.Title,
Language = entity.LanguageEntity?.Name,
Markdown = entity.Markdown,
UpdatedDate = entity.UpdatedDate.DateTime,
HtmlContent = entity.HtmlContent,
PlainTextContent = entity.PlainTextContent,
Slug = entity.Slug,
WordCount = entity.WordCount,
PublishedDate = entity.PublishedDate.DateTime,
Languages = languages ?? Array.Empty<string>()
};
}
}
Hier zie je dat ik de kaart maak. BlogPostEntity Tot mijn hoofd. BlogPostDto Dat is mijn overdrachtsobject.
Het doel is dat de front-end-diensten niets weten over het entiteitsobject en 'abstract zijn van de onderliggende datastructuur.
In deze toplevel-diensten heb ik dan de code om deze Dtos te tonen naar ViewModels die vervolgens gebruikt worden in de Controllers.
public static PostListModel ToPostListModel(this BlogPostDto dto)
{
return new PostListModel
{
Id = dto.Id,
Title = dto.Title,
Language = dto.Language,
UpdatedDate = dto.UpdatedDate.DateTime,
Slug = dto.Slug,
WordCount = dto.WordCount,
PublishedDate = dto.PublishedDate,
Languages = dto.Languages
};
}
public static BlogPostViewModel ToViewModel(this BlogPostDto dto)
{
return new BlogPostViewModel
{
Id = dto.Id,
Title = dto.Title,
Language = dto.Language,
Markdown = dto.Markdown,
UpdatedDate = dto.UpdatedDate.DateTime,
HtmlContent = dto.HtmlContent,
PlainTextContent = dto.PlainTextContent,
Slug = dto.Slug,
WordCount = dto.WordCount,
PublishedDate = dto.PublishedDate,
Languages = dto.Languages
};
}
Nogmaals, met een Mapper-tool kan je deze boilerplatencode vermijden ( en fouten verminderen ) maar ik vind dat dit aanpak goed werkt voor mij in dit geval.
Nu heb ik al deze modellen opgezet Ik kan controllermethoden toevoegen om ze te gebruiken. I' Ik zal deze stroom in het volgende deel behandelen.
In slotting:
We hebben nu de structuur die ons in staat stelt newsletterabonnementen aan onze gebruikers te bieden. Deze refactoring was een beetje pijnlijk, maar het is een feit van leven voor de meeste projecten. Volgende posten zullen de rest van dit , bevatten, inclusief een navolging van mijn gebruik van MSK. [FluentMail] om de e-mails en de gehosteerde dienst te sturen...en misschien een beetje meer Hangfire.